看报表

公司财报包含了资产负债表、利润表、现金流量表

一、资产负债表,就好像是一个企业的体检报告,体现了企业的实力和规模。

资产负债表的结构,主要围绕着一个恒等式搭建,即“资产等于负债加所有者权益”。

在资产负债表的左边,是资产。我们也可以这么理解资产,那就是,企业把钱用到哪里去了,也就是钱的去处。

资产负债表的右边,是负债和所有者权益。负债和所有者权益,也可以理解为钱是哪里来的,即钱的来源。负债,就是问别人借的钱。所有者权益,就是股东出的钱。

(举例:你说你在北京有一套房,总价值300万,自己首付100万,贷款200万。那么对应着财报里的资产负债表,你有300万的资产,负债200万,所有者权益是100万。所以,资产=负债+所有者权益。这个公式,是永远成立的。)

1、在资产中,还可以进一步划分为流动资产,和非流动资产。

(1)流动资产一般形容的就是流动性比较好的资产,很容易变现的资产,很快就可以变成现金的资产。但是流动资产本身也是在经营过程中不断转化的。

现金——存货——应收账款——现金

流动资产不可能永远是固定形态,钱要去买生产所需的东西,然后做出产品(存货),然后一部分成为现金,另外 一部分成为应收账款,

应收账款就是人家给你打白条,就是赊账

预付款是提前给供货商的钱,相当于预订,太大不好,证明公司地位不高,人家先收钱才敢给你货,而且如果预付款增长大,且不是货款,是些奇怪的专利工程款之类的,很可能有财务造假。

公司持有的股权投资如果分配股利,就是应收股利

很多集团公司是很大的,不仅仅是主营业务,还有很多很多小业务,这些和主营无关的小业务就是其他应收款,从造假的角度来讲,这些乱七八糟的小业务太容易藏污纳垢,

时间比较长租给人家要收回来的钱,比如你有飞机,工程机械,车,工厂,设备这些你租给人家,人家慢慢给你钱,这些都是长期应收款

企业准备以后卖出去的货就是存货。

那些还没有生产出来的原材料也半成品也是存货。

存货在不断增加,但是销售没有增加,这背后很可能是企业的经营出现了大问题,产品淘汰,或者过时,或者市场占有率下降,都非常有可能。或者根本就是无法销售的原料,半成品,次品,如存货总是消不掉,是非常危险的,存货是会跌价,会贬值的。

(2)非流动资产就是所有流动资产以外的资产,从属性上来说就是流动性不太好的资产,一年左右无法变现的资产

使用寿命一年以上的,为了经营而持有的就是固定资产。房子,建筑物,机器,设备,机械,运输工具,工具,器械都是。固定资产是需要计提的,买资产的时候需要付钱,之后购买的成本慢慢的摊销在资产的生命周期里,每年固定资产的计提折旧都要作为公司当期经营的费用从利润表里面扣除的,除了折旧以外还要每年进行减值测试,只要是损坏的,跌价,长期不用的都要计提减值准备,也是要从利润表里面扣除的。

因为要折旧所以好公司当然选择越来越快,早一点折旧完以后就不需要再折旧了,时间太长的折旧肯定不好,证明公司不赚钱,不敢折旧,怕影响利润,敢于快速折旧的公司才是优质的公司

,时间太长的折旧肯定不好,证明公司不赚钱,不敢折旧,怕影响利润,敢于快速折旧的公司才是优质的公司,如果一家企业固定资产长时间不增长或者缓慢,那么这个企业就已经在走下坡路了,因为这家企业已经找不到固定资产去购置了。固定资产增长太快要看利润能否跟上,注意有些企业已经很长时间业绩不增长,但是靠变卖固定资产来维持,这就非常危险了。

在建工程,还在建设的工程,建设好了就自动转成固定资产。但是在建工程是不需要计提折旧的,所以有些企业喜欢钻空子,故意不转成固定资产,为的就是躲避固定资产的折旧计提。一家公司长时间不把在建工程转为固定资产很可能就是在搞这个猫腻,甚至有些企业根本没有建设,完全是虚构了一个在建工程,在很多很多年以后再想办法大比例计提掉,这就是在造假,在转移资金了,

实体的资产就是无形资产。商标权,专利权,著作权,土地使用权,特许权,版权等都是无形资产。无形资产无法清点清楚,说不清楚,像这样的资产到底估值是多少是很有争议,如果一家企业无形资产太多,很可能在做虚假做大资产总量,至少你可以认为这家企业的资产结构很不健康,无形资产也要折旧,但是名字叫摊销,折旧和摊销的意义是一样的,只要确定了无形资产的使用时间,然后平均每年摊销多少就可以了,没办法搞清楚使用年限的就只能做减值准备

那些看不见,算不清,摸不着的但是可以产生收入的都是资产,因为可以赚钱,所以肯定是资产,但是会计科目不好搞定这些资产的描述,所以就叫商誉。收购公司的时候,如果公允价值1个亿,公司花2个亿买,那么超出的1个亿就放到商誉里面,商誉不需要计提,不需要摊销,但是需要减值准备,也就是说不需要和固定资产,无形资产那样每年去计提,但是每年年底需要减值准备一下,如果商誉的价值下降了就需要减值,对于商誉太高又经营不善的企业来说,商誉减值几乎是一定的,也是一个暗雷

长期待摊费用就是企业已经花出去的费用,但是可以持续一年的功效。比如说固定资产改良费用,改良了基本上用一年,不需要天天改良,这些就是可以持续一年以上功效的意思,修理费也是同理。但是有些公司刻意把长期摊销费用冒充资产,假装是一笔资产,把其他一些费用放在里面,这样就可以减少费用,增加利润,所以太多长期待摊费用肯定是不好的,很可能是别有用心的,

其他非流动资产就是不太好归类的资产,周转时间又超过了一年的就放在这里面,所有名字带“其他”的都很重要,都不应该数额太多,都需要去看看附注里面到底是什么,有没有猫腻。

2、负债按照期限的长短,分为流动性负债、和非流动性负债。

我们的资金来源,是有两种性质的。一种是借来的,叫债务,在财务里面叫做负债;一种是自己和股东们的自有资金投入,在财务里叫所有者权益。

所有者权益,又分为实收资本、资本公积(或盈余公积)、本年利润、未分配利润,

所谓实收资本,一般对应着企业刚设立时的注册资本。比如我企业的注册资金是100万,则意味着我公司被分成100万份,每个股东根据出资等情况,占有不同的份数。如果后来有投资人拿出100万投资你,其中11万计做实收资本,则实收资本总数达到111万;剩下的89万,就放到了资本公积科目里了(发行价高于实收资本的就放在这里面,资本公积可以用来送股),因为本年利润是归股东的嘛,所以计入本年利润。有时候,可能公司在前几年赚了很多钱,股东也不急着分红,就计了“未分配利润”

预收款项,在交易之前预先收取的部分货款,这个款项显示了企业的产品竞争力,应付职工薪酬就是工资,要给职工的一系列钱,奖金,保险,公积金,福利,教育经费,辞退补偿都是。区别就是一线员工的工资算是生产成本,工厂的员工算制造费用,管理层的算管理费用,销售人员的算销售费用,在建工程的员工算在建工程里面

负债是非常重要的,因为这个数据比较真实,伪造资产的方法很多,伪造负债的方法很少,所以负债总的来说很真实。

3、分析资产负债表,(1)、流动性资产最好要大于流动性负债。因为流动性负债是短期内要还的,你的流动性资产不能覆盖的话,企业的资金链就极度容易“咔嚓”断了。流动资产除以流动负债,这就是财务指标中的“流动比率”了,这个指标,通常最好要大于200%。

(2)、应收账款坏账计提政策是非常重要的,通常5年以上的坏账需要100%的计提,4年的坏账需要80%的计提,3年的坏账需要50%的计提2年的坏账需要30%的计提很多企业故意不计提坏账,导致利润虚胖的好看,

(3)、资产的总量不重要,不要用这个来判断企业的好坏,资产只要可以带来利润就是好资产。

(4)、有些企业是负债生存的是用杠杆来经营的,看上去负债总额很大包括预收款,这个也是在负债里面,但是这可不是有息负债,这是人家提前打钱给你,这是竞争力的体现,你不能简单的去看负债的总额,要看到底是负担的负债还是可以用来赚钱的负债。

二、利润表(损益表)里往往暗藏着玄机,由于权责发生制的关系,收入和支出不一定要有真实的现金流动,只要发生了,便进行确认,因此有可能产生空有纸面利润而没有现金流入的情况。利润表最易操控,

分析利润表应该关注哪些要点呢?解读和分析利润表有三组关键项目,分别如下。

营业收入和营业成本。毛利。三项费用(销售费用、管理费用和财务费用)。

只要把握这三组关键项目,就能抓住企业经营状况的核心,企业是持续发展还是陷入困境,一目了然。

企业没有收入就无法生存发展,但是收入必然伴随着成本的支出,两者相辅相成。

投资企业的第一步,就是要重点分析收入、成本以及两者之间的逻辑对应关系。因为几乎所有的财务造假舞弊的公司,都是为了做大营业收入,做小营业成本,尤其是主营业务。不少上市公司都是通过做大代理业务、做大流通、虚拟交易、一次性交易等做大营业收入的。

(优质的企业:收入的确认会在真正完成了交易,并且没有后顾之忧以后确认收入,劣质的企业:即使路边有个人告诉他可能会在一年后买他们公司的产品,他都会确认收入,所以收入这个数据太虚,太多想象空间,太多可能不对劲的地方,多看附注里面的具体内容,看看实实在在的收入有多少。)

看毛利率,要紧紧追踪行业水平,比较历史水平,以及根据自身的上下变动情况进行分析。

所谓三项费用就是指销售费用、管理费用和财务费用。很多公司往往把管理费用当成一个箩筐,将各种不知名的费用都往里装。而销售费用和营业收入之间有逻辑对应关系,财务费用就是企业为了经营筹集资金而花的钱,比如借钱需要的利息。财务费用不一定是负的,因为企业很多是没有有息负债的,并且还有利息收入,

除了以上三组关键项目以外,管理者还要重视企业的非经常性损益。所谓非经常性损益,就是与公司主营业务不直接相关的、偶然发生的一次性交易。这个数据对于投资者来说尤为有价值。非经常性损益往往被上市公司用来当作救命稻草,一旦发现经营状况不景气,就通过变卖股权、出售资产、寻求政府补贴、税收返还等方式和途径,营造一个看上去漂亮,但充斥着泡沫的故事。投资者应该剔除非经常性损益,

资产减值损失公司需要计提的资产减值损失有股权,固定资产,无形资产,商誉,存货,坏账,可供出售金融资产,持有至到期投资等。资产减值损失的计提是很多企业造假的主要方法,就是不计提,或者少量计提,就可以让利润虚胖。有时候还会为负,证明之前计提的东西都收回来了,一次性大额计提也不是好事,怎么企业在做财务洗澡,为了以后的业绩好看,并不是真正的经营有大改善。

公允价值变动损益收益这个就是交易性金融资产和投资性房地产的问题,当时买的金融资产或者房产,现在升值或者贬值了

营业利润一家优秀的企业应该利润来源于主营业务

营业外收入主营业务无关的收入,主要是处理一些资产,卖掉不需要的资产,或者是卖出无形资产,债务重组力得,企业合并损益,政府补助等这些都是。

营业外支出和主营业务无关的支出,资产的处理损失,固定资产盘亏,捐赠支出,罚款,报废等

净利润就是营业利润加上营业外收入再减去营业外支出再减所得税,剩下的就是净利润。但是净利润并不是公司真正赚到手的钱,利润表的质量要看现金流量表,没有现金流入的净利润是很虚的。

和资产负债表的质量要看利润表一样,不赚钱的资产是没有用的。经营现金流和净利润一样多,或者经营现金流更多,就证明企业的净利润质量非常高,不然就证明企业实际上没有赚这么多钱。只是从算账的角度来看企业赚了这么多钱,实际没有收到这么多现金。

利润表就是在告诉大家企业销售了多少东西出去,赚了多少钱,成本怎么样,费用怎么样。

说白了就是在证明资产负债表的资产结构和负债结构到底好不好。

如果利润表漂亮,就证明资产负债表其实是不错的,里面的很多结构搭配或许和教科书上的不一样,但是符合企业本身的经营特点,能赚钱的企业就是好企业,利润表好看自然就证明企业的资产负债表健康。

我们看利润表的重点就是多看附注,因为我们想看见更多数据背后的细节,我们不应该被表面的数据给蒙蔽双眼,一家企业利润表的真实性远远大过具体数据。

三、现金流量表

资产负债表——利润表——现金流量表,一家优秀的企业必须拥有充沛的现金,优质的利润空间,优秀的资产和负债结构。

现金流量表决定着企业的存亡。

因为企业可以没有利润,但是不能没有现金,没有利润证明暂时企业不行,产品没有市场,但是没有现金企业就要倒闭了。

利润是基于财务报表的,不是真实的现金,现金才是真正用来运作和经营的唯一保障。

现金流量表就是用来看清楚企业现金的重要报表,这张报表会告诉你现金怎么来的,怎么用的,还有多少。

经营活动现金流必须为正,并且持续增加这才是一家优秀企业该有的样子,如果一直下降,并且开始紧张,这样的企业肯定在经营方面出了问题。

投资活动现金流反映的是企业资本性支出的现金数额,企业买卖厂房,设备,固定资产都会导致投资活动现金流的增减。投资活动现金流大幅流出证明企业在进行大量的投资,一般来说处于扩张期的企业都需要大量的投资。投资活动现金流大幅流入,要看是投资收益,还是变卖资产,还是收回之前的投资,只要不是大量变卖资产,其他的都是正常的资金流动

现金流量表就是三个大方向,经营方面——经营活动现金流,投资方面——投资活动现金流,筹款方面——筹资活动现金流

最重要的肯定是经营活动,因为投资是为了经营,筹资也是为了经营,投资大部分情况就是扩建,就是扩大资产,这个是必要的,筹资就是借钱,

现金流量表其实很多都是对应的一进一出:

销售商品+提供劳务+费用返还+政府补贴+其他收入+收回投资+投资收益+处置资产+发行股票+银行贷款+发行债券=现金的流入

购买商品+接受劳务+支付职工+支付费用+上交税费+购买资产+金融投资+其他投资+发放分红+支付利息+归还本金=现金的流出

现金流量表是收付实现制,而利润表和资产负债表都是权责发生制。

区别就在于你在本期看见的利润表和资产负债表都可能有水分,都可能提前被美化,很多利润被提前装进来,很多资产被提前确认,但是现金流量表是真实的本期的现金流动情况,就可以让你看见企业到底经营的怎么样,也可以证伪利润表和资产负债表的质量。

现金流量表综合分析:

经营现金净流+(增加) 投资现金净流+(增加) 融资现金净流+(增加)

这种情况说明企业不缺钱,经营可以赚钱,投资也不怎么花钱,融资还在收钱。就是各种收钱。但是有一个问题就是,企业既然经营可以赚钱,投资又不花钱,为什么还需要融资呢?这个问题就有点严重,很可能企业隐藏了经营的真实情况,企业是缺钱的,要不就是有人在转移上市公司的现金,极少数可能是企业在准备搞一笔大投资,

经营现金净流+ 投资现金净流+ 融资现金净流-

这种情况说明企业经营是可以赚钱的,投资现金也可以有钱流入,但是融资是负的,证明企业在还债。企业是在用什么钱还债?是在变卖家产还是正常的经营现金还债?如果企业没有变卖家产,投资现金流的流入是利息收入,那就是健康的企业,企业在正常的还债,但是企业投资现金流为正说明了企业没有投资了,企业不再招兵买马了,并且企业还在还债,证明企业目前发展到了瓶颈,未来业绩可能很难增长

经营现金净流+ 投资现金净流- 融资现金净流+

这种情况说明企业经营赚到的钱再加上借的钱都投入到了投资当中,所以投资现金流是负的。这种企业一般处于扩张期企业需要投资,需要用钱,并且经营获得的钱不够,所以还要借钱来投资,要注意企业借钱的利息高不高,投资什么项目,如果项目不错未来有巨大的赚钱潜力则问题不大。

经营现金净流+ 投资现金净流- 融资现金净流-

这种情况说明企业经营可以赚钱,也在投资项目,并且还在还债。这种企业算是最优秀的企业

经营现金净流- 投资现金净流+ 融资现金净流+

这种情况说明企业的经营不赚钱,投资方面在卖家产或者是利息收入,并且还在借钱,要看企业到底经营能不能改善,并且要看企业的投资现金流到底是怎么来的,如果是变卖家产来的,基本上也是快倒闭了,

经营现金净流- 投资现金净流+ 融资现金净流-

这种情况说明企业经营不赚钱,但是要还债,只能变卖家产。

经营现金净流- 投资现金净流- 融资现金净流+

说明企业经营不赚钱,并且还在投资,还在借钱。

经营现金净流- 投资现金净流- 融资现金净流-

说明企业经营不赚钱,但是还在投资,并且还在还债。非常危险

,所有者权益变动表

所有者权益变动表就是股东权益和净资产的变动记录,目的就是告诉你企业的股东权益有多少增加或者减少,盈余还有多少,分红多少

五、财务报表附注分析

企业的经营细节都在附注里面,未来附注会越来越重要,数字会越来越被看轻,追本溯源是未来财务报表的发展方向,对于真正的高手投资者来说肯定会反复研究附注,新手比较重视数据,高手重视数据背后的内容。附注会详细说明存货的构成,应收账款的账龄,长期投资对象,借款的期限等,这些都是非常重要的数据。

六,企业的偿债能力分析

公司的偿债能力最主要体现在一年左右可以变现的资产,

流动比率(可以反映企业短期的偿债能力)=流动资产÷流动负债x100%

流动比率在2左右都不错,不需要追求过高。但是一定要记住,有些企业流动资产不高,是重资产企业,对于这些企业流动比率甚至不到1,但是他们依旧很优秀,因为他们可以赚钱,

速动比率(排除存货后的比率)=(流动资产-存货)÷流动负债

因存货变现能力相对较弱,为更好反应还债能力,就有速动比率,低于1的话,通常认为是短期偿债能力不是很好,1左右就不错了,不需要追求过高。

资产负债率是一个重要的长期偿债能力数据

资产负债率=负债总额÷资产总额x100%

通常把45%的负债率看做是一个界限,太低就说明企业不够激进,太高说明有一定风险。但是有些企业属于杠杆类企业,这些企业就是靠高杠杆赚钱的,所以不能用45%这个标准来衡量。资产负债率也要看清楚负债总额里面的内容,一般来说预收款是好事,可以排除后再算,负债还是重点看有息负债。

产权比率(也就是股东权益和负债总额的比率)=负债总额÷股东权益x100%

七,企业的营运能力(管理层能力)分析

营运能力的最高境界就是让现金在最短时间内变成更多的现金。

1、流动资产周转的判断包括:应收账款周转率,存货周转率,流动资产周转率

(1)、应收账款周转率(反映应收账款的周转情况)=(营业收入)销售收入÷平均应收账款x100%

这个数据主要是在告诉投资者,企业收到的收入里面有多少是打白条的,应收账款就是白条,优秀的公司应该是应收账款很少,

(2)、存货周转率(反映企业存货的周转情况):

【1】成本基础的存货周转率=营业成本÷存货平均余额x100%

即一定时期内企业销货成本与存货平均余额间的比率,它反映企业流动资产的流动性,主要用于流动性分析。

【2】收入基础的存货周转率=营业收入÷存货平均余额x100%

其中:存货平均余额=(期初存货+期末存货)/2

主要用于获利能力分析。存货是流动性很差的,因为存货还有原材料这些,半成品这 些,或者是积压的,过时的,淘汰的产品,存货还会贬值。这些东西很难变现的,所以存货周转率很重要!库存尽可能小一点,并且存货周转率高一点,

(3)、流动资产周转率=(主营业务收入)销售收入÷流动资产x100%

越大证明流动资产的质量好,可以带来很多的收入,

2、固定资产周转率=销售收入(营业收入)÷固定资产x100%

越大就证明固定资产很好,可以带来很多收入

八、盈利能力分析

营业利润率=(营业收入-营业成本-三费)÷营业收入

这个比率就是要你看清楚企业在扣除各种费用和成本以后,到底还能留下多少收入,

毛利率=(营业收入-营业成本)÷营业收入x100%

企业的竞争力在毛利率上面体现的淋漓尽致,一家企业如果毛利率不断下降证明企业的产品在慢慢失去竞争力。越高就越容易赚钱,企业本身就有各种税费,各种成本,开销也很大,毛利率不高的话,净利润肯定不高。

净利率=净利润÷营业收入x100%

净利率高则说明企业的营业收入质量非常高,这样的企业,盈利能力非常强。

净资产收益率ROE=净利润÷净资产(所有权权益|股东权益)x100%

反映企业的股东权益的投资报酬率,是评价股东权益财务状况的重要指标。告诉你股东每投一块钱资本,可以获得多少的净利润,收益率越高,代表盈利能力越强,

净资产收益率还可以这样更加详细的拆分:

净资产收益率=(净利润÷销售收入)x(销售收入÷平均总资产)x(平均总资产÷净资产)

净利润÷销售收入(就是产品的净利润率)

销售收入÷平均总资产(就是总资产周转率)

平均总资产÷净资产(就是杠杆系数)

通过这三维数据可以更加清楚企业的情况,到底是靠产品利润赚钱还是靠产品周转赚钱还是靠杠杆赚钱。

企业要赚钱肯定有一个特色,要不就是产品利润巨大,要不就是资产周转极快,要不就是杠杆撬动更多资源。

企业要赚钱肯定有一个特色,要不就是产品利润巨大,要不就是资产周转极快,要不就是杠杆撬动更多资源。

九、企业的成长性分析

营业收入增长率=(本期营业收入-上期营业收入)÷上期营业收入

营业利润增长率=(本期营业利润-上期营业利润)÷上期营业利润

总资产增长率=(本期总资产-上期总资产)÷上期总资产

净资产增长率=(本期净资产-上期净资产)÷上期净资产

十、财报综合分析

1,看财务报表不能只看一年,最好是看5-15年的财务报表,要动态的分析一家企业的经营变化,

2,不仅仅要深入看透一家企业的年报,还要对比其他相同企业和该企业的数据,

3、不要只看企业有哪些规划,更要看最终实行的怎么样。

4,分析财务报表最忌讳照本宣科,只看死板的财务数据,而不结合公司的具体情况分析,不做辩证分析,一定要对具体的问题做具体分析,

5,分析财务报表也不能格局太小,还要结合当下的经济环境来分析,

6、不同行业的财务数据放在一起比较,这是没有意义的。

7,一定要具体考虑到该公司的会计核算方式,比如存货的销货成本,有些公司是先进先出,有些公司是后进后出,还是分批确认的。比如,处理固定资产的折旧,有些公司用平均折旧法,有些公司用加速折旧法,还有些公司用计划成本来计提折旧,这些不同的会计核算方式会导致最终的财务数据有很大差距,

8,物价是一个很容易被遗忘的重要影响因素

9,我们在对比相同行业的不同公司的各项财务数据时,不是要看数字大小,而是要想数字背后的原因,

十一、财务造假的迹象:

如果企业很有钱,还在借钱,这就可能有巨大暗雷,不可能有人自己的钱不用去用有利息的钱。

卖资产来获得一次性大额收入,要不就是一次性计提费用,导致一年的财务特别差,平时几年利润好看一些。

刻意减少员工工资来提高利润

利用重组来洗清之前的猫腻

计提、减值、跌价,折旧不充分

一次性大额计提做财务洗澡

企业的产品无法准确计量和估计,比如农林牧渔和生物类资产

应收账款莫名其妙大增

大客户退货或者离开

存货莫名其妙大增

长期待摊费用大增

企业承诺了很高很难完成的业绩目标

企业大买无形资产

在建工程总是不转为固定资产

关联交易混乱

财务报表突然延期

公司正在接受政府监管部门的调查

会计政策莫名其妙改变

业绩预测非常不准确

非常异常的影响业绩的突发事件

营业外收入大增,又说不清楚

销售费用和管理费用出现大幅波动

企业借钱的利息特别高

企业的突然投资一些之前没有在董事会提过的投资

企业突然换会计师事务所,突然改变财务报表发布时间,董事集体辞职,大股东减持,财务总监离职,主要供货商可疑或者不正规,跨行业收购。

其他应付款也是经常用来隐藏利润的科目,经常用来调节利润,比如说今年生意好就把一些利润调整到这个科目下面,以后生意不好的时候再拿出来。其他应付款科目包括所有“其他”名字的科目其实都是数额很小的科目,但凡是数额很大肯定就有猫腻。

全面的了解企业,探索财务造假的真正动机:最上层的动机就是品德问题,中层就是经营问题,公司的经营是否有问题?行业是否有问题?产品是否有问题?这些很可能会倒逼一些企业不得不做假。下层就是资金链问题,一家企业最有可能做假的时机就是倒闭之前,因为没有人会想违法乱纪,很多都是被逼的

抛开当期的财务报表,要立足长远,多年的财务报表,要看企业经营的怎么样,企业管理层是否靠谱,企业时候有资金链断裂风险,就像一个人是否会违法犯罪,不能看他当天的行为,要看这个人多年的行为,品德如何,是否人生有重大变故,是否特别缺钱等,这些才是最有可能导致这个人违法乱纪的根本原因。

冰冻三尺非一日之寒,企业在造假之前肯定是有一个开端的,比如行业开始不景气了,企业开始亏损了,借了很多利息高的钱,管理层大换水,这些都是诱因,都是开端,企业为了不倒闭,并且维持投资者的信任,维持金融机构的信任很可能就会开始走上业绩造假的道路。

财务常识其实非常重要,比如卖东西肯定需要成本和费用,比如你货买的好,但是运输费还减少了,这正常?

一定要注意证监会,上交所,深交所会经常给各种企业发询问函,其实就是发现了问题,

要识别虚假财务报表直接的手段就是看审计意见除了标准无保留意见以外的财务报表都不需要去看了,肯定有问题。

六、对于港股财报来说,需要我们重点关注的还是三大财务报表和董事会报告,综合权益及其他全面收入表即利润表,综合财务状况表即资产负债表,综合现金流量表也就是现金流量表。

那么这三张表有什么用呢,我们简单概括一下就是:

1、看资产负债表的时候,我们可以知道企业的钱,要么是借的,要么是股东投的。

2、看利润表时,我们发现钱还可以是自己挣的。

3、最后现金流量表,将企业的现金活动通过三大板块展示,即筹资现金活动,投资现金活动和经营活动现金流。

等额本息,等额本金

一般房贷还款方式的计算公式分为两种:

  1、等额本息计算公式:

  计算原则:银行从每月月供款中,先收剩余本金利息,后收本金;利息在月供款中的比例中随剩余本金的减少而降低,本金在月供款中的比例因增加而升高,但月供总额保持不变。

  2、等额本金计算公式:

  每月还款额=每月本金+每月本息

  每月本金=本金/还款月数

  每月本息=(本金-累计还款总额)X月利率

  计算原则:每月归还的本金额始终不变,利息会随剩余本金的减少而减少。

降息,LPR,MLF,SLF,SLO

降息、加息,都是以基准利率为调整标准,法定基准利率,还分为存款基准利率跟贷款基准利率。存贷款基准利率是央行制定的,给商业银行的贷款指导性利率,并非实际的贷款利率,而实际贷款利率,会高于法定基准利率。

降息简单的说就是降低基准利率,但是自2015年之后央行再也没有调整基准利率,比如目前5年期的贷款基准利率仍然是4.9%,已经有将近6年时间没有改变过。

原来国内房贷大多数是使用的人民银行贷款基准利率,改革后的房贷更趋向LPR利率。根据LPR利率(五年期)加一定点数自由浮动。

LPR,这是2019年10月8日之后正式实施的一种贷款基础利率,他由央行根据18家报价银行最近一个月的实际利率表现进行汇总统计,去掉一个最高报价和一个最低报价,然后进行加权平均之后得出一个数据,并在每月的20号对外公布。

从短期来看,LPR降低意味着大家借贷成本降低,不论是贷款买房或者申请经营性贷款或者消费贷款利息会跟着减少,这对于降低大家的资金成本,加快疫情之后恢复生产生活是非常有帮助的。

但从长远来看,这次LPR的降低对大家的影响是比较有限的,因为LPR它不是一个固定的利率,而是一个浮动的利率,每个月都有浮动,至于LPR是高是低,关键要看市场实际的资金需求情况,通常情况下市场资金需求比较紧张,对应的LPR就会上升,相反如果市场资金需求比较低迷,那LPR就会跟着降低。

而对于普通老百姓来说,LPR的变动主要跟大家的房贷有很大的关系,因为按照央行的相关文件要求,大家在跟银行签署房贷合同的时候是可以自行约定房贷利率调整周期的,最低一年就可以调整。这意味着大家在跟银行签完房贷合同之后,如果利率调整日期LPR有所变动的,银行就会根据最新的LPR对房贷利率进行调整,如果LPR降低了,大家的房贷利息会跟着下降,如果LPR上涨了,对应的房贷利息会跟着上升。

5年期LPR主要与基建项目利率、企业中长贷利率以及居民房贷利率挂钩,

“LPR=MLF+商业银行加点”2019年以来,5年期LPR调降均因MLF降息驱动,而20220520这次MLF按兵不动,主要靠银行让利以及压降存款端成本来实现,是在有限的政策条件下让利实体经济。

国家宏观调控经济的手段分为货币政策和财政政策,其中货币政策针对的主要是货币供应量和利率,央行需要把这两者控制在适合国家经济发展水平内,这就需要依靠各种工具和操作,MLF、SLF等就是具体的工具和操作

中期借贷便利MLF操作:MLF是央行的一种货币政策工具,用来提供中期基础货币,保障银行体系流动性总体平稳适度,支持货币信贷合理增长。主要面向符合宏观审慎管理要求的商业银行、政策性银行,以通过招标的方式开展。中期借贷便利利率发挥中期政策利率的作用,通过调节向金融机构中期融资的成本来对金融机构的资产负债表和市场预期产生影响,引导其向符合国家政策导向的实体经济部门提供低成本资金,

SLF:全称为常备借贷便利,它是中国人民银行正常的流动性供给渠道,主要功能是满足金融机构期限较长的大额流动性需求,在银行体系流动性出现临时性波动时相机运用。SLF主要面对的是政策性银行和全国性商业银行,期限为1-3个月,利率水平则根据货币政策调控、引导市场利率的需要等因素综合确定。央行运用SLF的方式是用抵押的方式发放,银行需要提供合格的抵押品,如高信用评级的债券类资产及优质信贷资产等。

SLO:全称为短期流动性调节工具。每周二、周四,央行一般都会进行公开市场操作,目前最主要的是回购操作。回购操作又分成两种,正回购和逆回购。SLO就是主要以期限在7天以内的正回购和逆回购为主,央行可以通过SLO来调节7天以内的超短期货币供应和利率,遇节假日可适当延长操作期限,采用市场化利率招标方式开展操作。MLF和SLF就是央行给商业银行钱。(逆回购为中国人民银行向一级交易商购买有价证券,并约定在未来特定日期将有价证券卖给一级交易商的交易行为,逆回购为央行向市场上投放流动性的操作,逆回购到期则为央行从市场收回流动性的操作。简单说就是主动借出资金,获取债券质押的交易就称为逆回购交易,此时投资者就是接受债券质押,借出资金的融出方。)

PSL:全称为抵押补充贷款工具,由央行在2014年创设,多是央行为了支持一些特定项目的建设向政策性银行(如国开行)提供的成本较低的抵押贷款,期限一般在3-5年;此外,它还可以用于向市场投放货币,调节中期市场利率。

OMO:全称为公开市场操作,是中央银行吞吐基础货币,调节市场流动性的主要货币政策工具,通过中央银行与指定交易商进行有价证券和外汇交易,实现货币政策调控目标。


债券

可转换债券是债券持有人可按照发行时约定的价格将债券转换成公司的普通股票的债券。如果债券持有人不想转换,则可以继续持有债券,直到偿还期满时收取本金和利息,或者在流通市场出售变现。如果持有人看好发债公司股票增值潜力,在宽限期之后可以行使转换权,按照预定转换价格将债券转换成为股票,发债公司不得拒绝。该债券利率一般低于普通公司的债券利率,企业发行可转换债券可以降低筹资成本。可转换债券持有人还享有在一定条件下将债券回售给发行人的权利,发行人在一定条件下拥有强制赎回债券的权利。可转换公司债券的期限最短为1年,最长为6年,自发行结束之日起6个月方可转换为公司股票。可转换公司债券应半年或1年付息1次

公司债,是指公司依照法定程序发行,约定在一定期限还本付息的有价证券,主体是公司。目前只在证券交易所流动,开放度较大,所以定价是发行公司和承销商通过市场询价确定的,用户支持公司运营,企业债多数是由央企,国企或国有控股企业发行的,流动场所主要在银行间市场流动。企业债发债基本上是为了基础设施建设和政府项目。

国债是由国家发行的债券,是中央政府为筹集财政资金而发行的一种政府债券,是中央政府向投资者出具的、承诺在一定时期支付利息和到期偿还本金的债权债务凭证。

国债利率是利息和本金之间的比率,依据金融市场各种证券的平均利率水平确定,一般略高于银行储蓄存款利率。和政府信用状况也有关系,且当社会资金供应充足时国债利率可降低。

国债收益率指每年获得收益占资本金的比率,与国债利率不同,不仅指利息收,还包括国债买卖的盈亏和国债利息再投入所得到的收益,又分直接收益率(年利息除以国债的市场价格),名义收益率(票面上显示的收益率),到期收益率(债券持有最终到期收益。到期收益率会受市场利率的影响,市场利率越低到期收益率可能会上升,市场利率上涨可能到期收益率会下降。),持有期收益率。

经济越好,投资机会越多,国债价格越便宜,国债的收益率也就越高。也就是说,美国十年期国债收益率的上涨可能意味着美国实体经济已经处在恢复当中。如果已发行的老国债收益率上升的话,国家再发行新国债要想获得足额认购会怎么办呢?只能提高新发国债的利率来提高投资者的购买热情。这就出现了新发短期国债利率的提升。要知道,国债利率往往是一个国家的基准利率(如中美利差倒挂指的是中美10年期国债收益率出现倒挂,美变得比中高了)。国债率提升,将带动整个市场的利率水平提升,说直白点就是起到了加息的效果。显然,加息构成的就是整个市场的利空逻辑,资本市场往往下跌回调。

(举例来讲,比如张三购买了100张票面价格100元的10年期国债,票面利率为年息3%,共计花费了10000元。那么对于张三来讲,持有到期国家还本付息对应的是每年3%的收益率。

但是,张三购买国债的第二年,发现了市场中一门更好的生意,预计投资收益率5%,于是张三就想把投资国债的10000元收回来,去投资这门生意。但是国债还没到期呢,怎么办?没关系。除了国家兑付外,张三可以把手里的国债卖给别人,比如李四。但是呢,李四也看到了市场中5%回报的投资机会。如果张三把国债按照100元/张的票面发行价格转让给李四,那么李四的收益率只有3%,李四也会觉得不划算。

这样的话,张三可以99元/张的价格折价把国债卖给李四,李四持有国债的收益率就没张多出来1元钱。这1元钱就是99元交易价格与国债票面价格100元之间的差价。如此,李四的每张国债每年收益就变成了4元(大致意思,实际应平均到后续几年),此时李四购买的国债收益率就变成了4元/99元,接近4%。

这样一个把国债拿到二级市场交易的过程,就让国债的收益率从3%涨到了4%。大家可以看到,国债收益率上升对应的是国债价格下降的过程。也就是,国债价格越便宜,国债收益率越高。国债价格越贵,国债收益率越低。)

一般而言该收益率越高表明这一国家的经济增速越高,

美债收益率倒挂,正常情况下,期限越长的债券,收益率应该越高;期限越短,收益率应该越低。这种现象在美国这种利率自由市场才有意义,我们一般讨论的也指的是美债。

逻辑上来讲,利率倒挂通常反映人们的悲观预期。要知道美联储通常影响的是短期利率,当人们预期经济即将不好,美联储不再加息甚至未来可能降息,那么短期利率将会下降,做短期投资的再投资风险会更大,这时候人们为了锁定收益,纷纷买入长期国债,买买买使得10年期国债价格上升,收益率下降,降降降使得最后出现了长期利率比短期利率还低的现象。

次级债券,是指偿还次序优于公司股本权益、但低于公司一般债务的一种债务形式。各种证券的求偿权优先顺序为:一般债务 >次级债务> 优先股 > 普通股

债券收益率是投资于债券上每年产生出的收益总额与投资本金总量之间的比率。决定债券收益率的要素主要有三个: 即利率、期限、购买价格。这三个要素之间的变动决定了债券收益率的高低。一般来说,债券给投资者带来的收入有以下三种: ①按债券的既定利率计算的利息收入。②认购价格与偿还价格 (票面价格) 之间的差益差损。③将所得利息和差益差损等再投资的收益。按照这三种不同的收入分别计算收益率,可分为直接收益率,单利最终收益率和复利最终收益率,直接收益率是按既定利率计算的年利息收入与投资本金的比率。

国际贸易基本上是非现金结算,汇票指是出票人签发,委托付款人在见票时或者在指定日期无条件支付确定的金额给收款人或者持票人的票据。

本票我国指银行本票,指出票人签发的,承诺自己在见票时无条件支付确定金额给收款人或者持票人的票据,国外允许企业和个人签发本票。

支票指发票人签发的委托银行等金融机构于见票时支付一定金额给收款人或者其他指定人的一种票据,可分记名,不记名,保付,划线支票等

科创板推做市商制度

我们国内目前的证券期货交易,采用的都是竞价交易制度——投资者通过网络,把买卖指令传输到交易所,交易所的电脑主机根据时间优先、价格优先的原则,将买卖指令撮合成交,形成连续的成交价格。根据这种交易方式下价格的形成机理,又可以叫做指令驱动(Order driven)制度

做市商制度是一种市场交易制度,由具备一定实力和信誉的法人充当做市商,不断地向投资者提供买卖价格,并按其提供的价格接受投资者的买卖要求,以其自有资金和证券与投资者进行交易,从而为市场提供即时性和流动性,并通过买卖价差实现一定利润。简单说就是:报出价格,买卖双方不必等到交易对手的出现,就能按这个价格买入或卖出。是由做市商为投资者提供买卖双边报价进行对赌交易,通过报价的更新来引导成交价格发生变化,所以叫做报价驱动(Quote driven)制度

垄断型的做市商制度,即每只证券有且仅有一个做市商,这种制度的典型代表是纽约证券交易所。因其垄断性通常也可以获得高额利润;竞争型的做市商制度,在一定程度上允许做市商自由进入或退出,这种制度的典型代表是美国的NASDAQ系统,

优点:提高流动性,增强市场吸引力;稳定市场,促进市场平衡运行;抑制价格操纵;校正买卖指令不均衡现象;有价格发现的功能。

字符编码

摘自https://liaoxuefeng.com/books/python/basic/string-encoding/index.html

字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。

因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),如果要表示更大的整数,就必须用更多的字节。比如两个字节可以表示的最大整数是65535,4个字节可以表示的最大整数是4294967295

由于计算机是美国人发明的,因此,最早只有127个字符被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122

但是要处理中文显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。

你可以想得到的是,全世界有上百种语言,日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。

char-encoding-problem

因此,Unicode字符集应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

Unicode标准也在不断发展,但最常用的是UCS-16编码,用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode。

现在,捋一捋ASCII编码和Unicode编码的区别:ASCII编码是1个字节,而Unicode编码通常是2个字节。

字母A用ASCII编码是十进制的65,二进制的01000001

字符0用ASCII编码是十进制的48,二进制的00110000,注意字符'0'和整数0是不同的;

汉字已经超出了ASCII编码的范围,用Unicode编码是十进制的20013,二进制的01001110 00101101

你可以猜测,如果把ASCII编码的A用Unicode编码,只需要在前面补0就可以,因此,A的Unicode编码是00000000 01000001

新的问题又出现了:如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。

所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:

字符ASCIIUnicodeUTF-8
A0100000100000000 0100000101000001
x01001110 0010110111100100 10111000 10101101

从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。

搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:

在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码

用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:

rw-file-utf-8

浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:

web-utf-8

所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />的信息,表示该网页正是用的UTF-8编码。

Python的字符串

搞清楚了令人头疼的字符编码问题后,我们再来研究Python的字符串。

在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言,例如:

>>> print('包含中文的str')
包含中文的str

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'

如果知道字符的整数编码,还可以用十六进制这么写str

>>> '\u4e2d\u6587'
'中文'

两种写法完全是等价的。

由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes

Python对bytes类型的数据用带b前缀的单引号或双引号表示:

x = b'ABC'

要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:

>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。

bytes中,无法显示为ASCII字符的字节,用\x##显示。

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:

>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

如果bytes中包含无法解码的字节,decode()方法会报错:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
Traceback (most recent call last):
  ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'

要计算str包含多少个字符,可以用len()函数:

>>> len('ABC')
3
>>> len('中文')
2

len()函数计算的是str的字符数,如果换成byteslen()函数就计算字节数:

>>> len(b'ABC')
3
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
6
>>> len('中文'.encode('utf-8'))
6

可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。

在操作字符串时,我们经常遇到strbytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对strbytes进行转换。

由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;

第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。

申明了UTF-8编码并不意味着你的.py文件就是UTF-8编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM编码:

set-encoding-in-notepad++

如果.py文件本身使用UTF-8编码,并且也申明了# -*- coding: utf-8 -*-,打开命令提示符测试就可以正常显示中文:

py-chinese-test-in-cmd

格式化

最后一个常见的问题是如何输出格式化的字符串。我们经常会输出类似'亲爱的xxx你好!你xx月的话费是xx,余额是xx'之类的字符串,而xxx的内容都是根据变量变化的,所以,需要一种简便的格式化字符串的方式。

py-str-format

在Python中,采用的格式化方式和C语言是一致的,用%实现,举例如下:

>>> 'Hello, %s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

你可能猜到了,%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略。

常见的占位符有:

占位符替换内容
%d整数
%f浮点数
%s字符串
%x十六进制整数

其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:

print('%2d-%02d' % (3, 1))
print('%.2f' % 3.1415926)

如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串:

>>> 'Age: %s. Gender: %s' % (25, True)
'Age: 25. Gender: True'

有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%

>>> 'growth rate: %d %%' % 7
'growth rate: 7 %'

format()

另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}{1}……,不过这种方式写起来比%要麻烦得多:

>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'

f-string

最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:

>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62

Python疑点-复习用

摘自学习时https://liaoxuefeng.com/books/python/introduction/index.html

一、

理解变量在计算机内存中的表示也非常重要。当我们写:

a = 'ABC'

时,Python解释器干了两件事情:

  1. 在内存中创建了一个'ABC'的字符串;
  2. 在内存中创建了一个名为a的变量,并把它指向'ABC'

也可以把一个变量a赋值给另一个变量b,这个操作实际上是把变量b指向变量a所指向的数据

a = ‘ABC’
b = a
a = ‘XYZ’
print(b)

b的值是'ABC',因为执行a = 'XYZ',解释器创建了字符串’XYZ’,并把a的指向改为'XYZ',但b并没有更改

摘自https://liaoxuefeng.com/books/python/basic/data-types/index.html

再议不可变对象

上面我们讲了,str是不变对象,而list是可变对象。

对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:

>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']

而对于不可变对象,比如str,对str进行操作呢:

>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'

虽然字符串有个replace()方法,也确实变出了'Abc',但变量a最后仍是'abc',应该怎么理解呢?

我们先把代码改成下面这样:

>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'

要始终牢记的是,a是变量,而'abc'才是字符串对象!有些时候,我们经常说,对象a的内容是'abc',但其实是指,a本身是一个变量,它指向的对象的内容才是'abc'

┌───┐                  ┌───────┐
│ a │─────────────────>│ 'abc' │
└───┘                  └───────┘

当我们调用a.replace('a', 'A')时,实际上调用方法replace是作用在字符串对象'abc'上的,而这个方法虽然名字叫replace,但却没有改变字符串'abc'的内容。相反,replace方法创建了一个新字符串'Abc'并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串'abc',但变量b却指向新字符串'Abc'了:

┌───┐                  ┌───────┐
│ a │─────────────────>│ 'abc' │
└───┘                  └───────┘
┌───┐                  ┌───────┐
│ b │─────────────────>│ 'Abc' │
└───┘                  └───────┘

所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

二、

列表:list是一种有序的集合,可以随时添加和删除其中的元素,用中括号[]括起来

元组:tuple和list非常类似,但是tuple一旦初始化就不能修改,用小括号()括起来,也没有append(),insert()这样的方法。

要定义一个只有1个元素的tuple,如果你这么定义:

>>> t = (1)
>>> t
1

定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1

所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:

>>> t = (1,)
>>> t
(1,)

Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号。

最后来看一个“可变的”tuple:

>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

这个tuple定义的时候有3个元素,分别是'a''b'和一个list,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a',就不能改成指向'b',指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!

这两个都是可迭代的。

for…in循环,依次把list或tuple中的每个元素迭代出来,看例子:

names = ['Michael', 'Bob', 'Tracy']
for name in names:
    print(name)

执行这段代码,会依次打印names的每一个元素:

Michael
Bob
Tracy

所以for x in ...循环就是把每个元素代入变量x,然后执行缩进块的语句。

再比如我们想计算1-10的整数之和,可以用一个sum变量做累加:

sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
    sum = sum + x
print(sum)

Python的for循环不仅可以用在listtuple上,还可以作用在其他可迭代对象上,比如dict

但因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()

我们已经知道,可以直接作用于for循环的数据类型有以下几种:

一类是集合数据类型,如listtupledictsetstr等;

一类是generator,包括生成器和带yield的generator function。

这些可以直接作用于for循环的对象统称为可迭代对象Iterable

可以使用isinstance()判断一个对象是否是Iterable对象:

>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

可以被next()函数调用并不断返回下一个值的对象称为迭代器Iterator

可以使用isinstance()判断一个对象是否是Iterator对象:

>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator

listdictstrIterable变成Iterator可以使用iter()函数:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

你可能会问,为什么listdictstr等数据类型不是Iterator

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算

三、

if语句执行有个特点,它是从上往下判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elifelse

另外当我们用if ... elif ... elif ... else ...判断时,会写很长一串代码,可读性较差。

如果要针对某个变量匹配若干种情况,可以使用match语句。

例如,某个学生的成绩只能是ABC,用if语句编写如下:

score = 'B'
if score == 'A':
    print('score is A.')
elif score == 'B':
    print('score is B.')
elif score == 'C':
    print('score is C.')
else:
    print('invalid score.')

如果用match语句改写,则改写如下:

score = 'B'

match score:
    case 'A':
        print('score is A.')
    case 'B':
        print('score is B.')
    case 'C':
        print('score is C.')
    case _: # _表示匹配到其他任何情况
        print('score is ???.')

使用match语句时,我们依次用case xxx匹配,并且可以在最后(且仅能在最后)加一个case _表示“任意值”

match语句还可以匹配列表,功能非常强大。

我们假设用户输入了一个命令,用args = ['gcc', 'hello.c']存储,下面的代码演示了如何用match匹配来解析这个列表:

args = ['gcc', 'hello.c', 'world.c']
# args = ['clean']
# args = ['gcc']

match args:
    # 如果仅出现gcc,报错:
    case ['gcc']:
        print('gcc: missing source file(s).')
    # 出现gcc,且至少指定了一个文件:
    case ['gcc', file1, *files]:
        print('gcc compile: ' + file1 + ', ' + ', '.join(files))
    # 仅出现clean:
    case ['clean']:
        print('clean')
    case _:
        print('invalid command.')

第一个case ['gcc']表示列表仅有'gcc'一个字符串,没有指定文件名,报错;

第二个case ['gcc', file1, *files]表示列表第一个字符串是'gcc',第二个字符串绑定到变量file1,后面的任意个字符串绑定到*files(符号*的作用将在函数的参数中讲解),它实际上表示至少指定一个文件;

第三个case ['clean']表示列表仅有'clean'一个字符串;

最后一个case _表示其他所有情况。

四、

摘自https://liaoxuefeng.com/books/python/function/parameter/index.html

默认参数有个最大的坑,演示如下:

先定义一个函数,传入一个list,添加一个END再返回:

def add_end(L=[]):
    L.append('END')
    return L

当你正常调用时,结果似乎不错:

>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']

当你使用默认参数调用时,一开始结果也是对的:

>>> add_end()
['END']

但是,再次调用add_end()时,结果就不对了:

>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

很多初学者很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了'END'后的list。

原因解释如下:

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

 特别注意

定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

现在,无论调用多少次,都不会有问题:

>>> add_end()
['END']
>>> add_end()
['END']

为什么要设计strNone这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。

我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:

def calc(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

但是调用的时候,需要先组装出一个list或tuple:

>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84

如果利用可变参数,调用函数的方式可以简化成这样:

>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84

所以,我们把函数的参数改为可变参数:

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

>>> calc(1, 2)
5
>>> calc()
0

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:

>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14

这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

>>> nums = [1, 2, 3]
>>> calc(*nums)
14

*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

函数person除了必选参数nameage外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:

>>> person('Michael', 30)
name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到nameage这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

当然,上面复杂的调用可以用简化的写法:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

命名关键字参数

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。

仍以person()函数为例,我们希望检查是否有cityjob参数:

def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

但是调用者仍可以传入不受限制的关键字参数:

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收cityjob作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):
    print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

调用方式如下:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

def person(name, age, *args, city, job):
    print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'

由于调用时缺少参数名cityjob,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。

命名关键字参数可以有缺省值,从而简化调用:

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)

由于命名关键字参数city具有默认值,调用时,可不传入city参数:

>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:

def person(name, age, city, job):
    # 缺少 *,city和job被视为位置参数
    pass

参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数

默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))

关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

五:

Python内置的sorted()函数就可以对list进行排序:

>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]

此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

>>> sorted([36, 5, -12, 9, -21], key=abs) [5, 9, -12, -21, 36]

key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序

可实现忽略大小写的排序:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']

六、

Python内建的filter()函数用于过滤序列。

map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

例如,在一个list中,删掉偶数,只保留奇数,可以这么写:

def is_odd(n):
    return n % 2 == 1

list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]

把一个序列中的空字符串删掉,可以这么写:

def not_empty(s):
return s and s.strip()

list(filter(not_empty, [‘A’, ”, ‘B’, None, ‘C’, ‘ ‘]))
# 结果: [‘A’, ‘B’, ‘C’]

s and s.strip()“ 这个表达式的值。Python语法是这么运行的:如果s is None;那么s会被判断为False。而False不管和什么做and;结果都是False;所以不需要看and后面的表达式;直接返回;注意不是返回False;。如果s is not None;那么s会被判断为True;而True不管和什么and都返回后一项。于是就返回了s.strip()。所以s.strip() 不能单独使用;语法是有问题的。

可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。

注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

七、

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

调用函数f时,才真正计算求和的结果:

>>> f()
25

在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1()f2()的调用结果互不影响。

闭包

注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。

你可能认为调用f1()f2()f3()结果应该是149,但实际结果是:

>>> f1()
9
>>> f2()
9
>>> f3()
9

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

再看看结果:

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

缺点是代码较长,可利用lambda函数缩短代码

nonlocal

使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:

def inc():
    x = 0
    def fn():
        # 仅读取x的值:
        return x + 1
    return fn

f = inc()
print(f()) # 1
print(f()) # 1

但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:

def inc():
    x = 0
    def fn():
        # nonlocal x
        x = x + 1
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

原因是x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1

 提示

使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。

八、装饰器Decorator



由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
>>> def now():
...     print('2024-6-1')
...
>>> f = now
>>> f()
2024-6-1

函数对象有一个__name__属性(注意:是前后各两个下划线),可以拿到函数的名字:

>>> now.__name__
'now'
>>> f.__name__
'now'

现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

@log
def now():
    print('2024-6-1')

调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:

>>> now()
call now():
2024-6-1

@log放到now()函数的定义处,相当于执行了语句:

now = log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

这个3层嵌套的decorator用法如下:

@log('execute')
def now():
    print('2024-6-1')

执行结果如下:

>>> now()
execute now():
2024-6-1

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

>>> now = log('execute')(now)

我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者针对带参数的decorator:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

import functools是导入functools模块。模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可

九、

mylist=[“a”,”b”,”c”,”a”,”b”] #定义一个列表

print(mylist.index(“c”,0,4)) #查找指定列表下标范围的元素”c”,并返回找到对应数据的下标,范围区间是左闭右开【0,4),也就是包含0但不包含4,

此处显示的结果为 2

tup1=(“aa”,”bb”,22,”dd”,”ee”,”ff”)

print(tup1[1:4]) # 对元组进行切片,也是左闭右开,结果为(“bb”,22,”dd”)

一个0-99的数列

前10个数,每两个取一个:
>>> L[:10:2]
[0, 2, 4, 6, 8]
后10个数
>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

十、

continue语句,跳过当前的这次循环,直接开始下一次循环。

break语句可以提前退出当前循环。

十一、字典

#用大括号定义字典,其实就是多个键-值(key:value)对,在其他语言中也称为map,

info={“name”:”刘德华”,”age”:56,”sex”:”man”}

#用中括号里写KEY值进行字典的value值的访问,结果为“刘德华”

print(info[“name”])

#如果用print(info[“gender”])直接查找,因为没有这个键,所以会报错,我们用以下get方法来查找,后面的”m”为没找到链的时候,设置成的可以返回的默认值。

print(info.get(“gender”,”m”))

#删除指定的键-值(key:value)对,不用方法,直接用del,下面把 “name”:”刘德华” 这个键-值(key:value)对删除

del info[“name”]

dict内部存放的顺序和key放入的顺序是没有关系的。

正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,用{x,y,z,...}列出每个元素:

>>> s = {1, 2, 3}
>>> s
{1, 2, 3}

或者提供一个list作为输入集合:

>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

重复元素在set中自动被过滤:

>>> s = {1, 1, 2, 2, 3, 3}
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通过remove(key)方法可以删除元素:

>>> s.remove(4)
>>> s
{1, 2, 3}

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。

十二、

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))

>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:

>>> L = []
>>> for x in range(1, 11):
...    L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

三层和三层以上的循环就很少用到了。

运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

>>> import os # 导入os模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录

十三 生成器

摘自https://liaoxuefeng.com/books/python/advanced/generator/index.html

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。

我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4
9
16
25
36
49
64
81

所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, …

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

注意,这是一个赋值语句,你应该分等号的两边分开来看:

a, b = b, a + b

相当于:

t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]

但不必显式写出临时变量t就可以赋值。

上面的函数可以输出斐波那契数列的前N个数:

>>> fib(6)
1
1
2
3
5
8
'done'

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator函数,只需要把print(b)改为yield b就可以了:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

这里,最难理解的就是generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

举个简单的例子,定义一个generator函数,依次返回数字1,3,5:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)

调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值:

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。

请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。

有的童鞋会发现这样调用next()每次都返回1:

>>> next(odd())
step 1
1
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1

原因在于odd()会创建一个新的generator对象,上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()当然每个都会返回第一个值。

正确的写法是创建一个generator对象,然后不断对这一个generator对象调用next()

>>> g = odd()
>>> next(g)
step 1
1
>>> next(g)
step 2
3
>>> next(g)
step 3
5

回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8

十四

把函数作为参数传入,这样的函数称为高阶函数

Python内建了map()reduce()函数。

如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。

我们先看map。map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

            f(x) = x * x

                  │
                  │
  ┌───┬───┬───┬───┼───┬───┬───┬───┐
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   2   3   4   5   6   7   8   9 ]

  │   │   │   │   │   │   │   │   │
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   4   9  16  25  36  49  64  81 ]

现在,我们用Python代码实现:

>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。

你可能会想,不需要map()函数,写一个循环,也可以计算出结果:

L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
    L.append(f(n))
print(L)

的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?

所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:

>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

只需要一行代码。

再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

比方说对一个序列求和,就可以用reduce实现:

>>> from functools import reduce
>>> def add(x, y):
...     return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25

十五 匿名函数lambda

当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:

>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]

通过对比可以看出,匿名函数lambda x: x * x实际上就是:

def f(x):
    return x * x

关键字lambda表示匿名函数,冒号前面的x表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25

同样,也可以把匿名函数作为返回值返回,比如:

def build(x, y):
    return lambda: x * x + y * y

十六

每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包,无法被别的文件引用。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是目录名

十七

在类中,__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,

十八 多态

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型

判断一个变量是否是某个类型可以用isinstance()判断:

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

看来abc确实对应着listAnimalDog这3种类型。

但是等等,试试:

>>> isinstance(c, Animal)
True

看来c不仅仅是Dogc还是Animal

不过仔细想想,这是有道理的,因为Dog是从Animal继承下来的,当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种!

所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:

>>> b = Animal()
>>> isinstance(b, Dog)
False

Dog可以看成Animal,但Animal不可以看成Dog

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

def run_twice(animal):
    animal.run()
    animal.run()

当我们传入Animal的实例时,run_twice()就打印出:

>>> run_twice(Animal())
Animal is running...
Animal is running...

当我们传入Dog的实例时,run_twice()就打印出:

>>> run_twice(Dog())
Dog is running...
Dog is running...

当我们传入Cat的实例时,run_twice()就打印出:

>>> run_twice(Cat())
Cat is running...
Cat is running...

看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')

当我们调用run_twice()时,传入Tortoise的实例:

>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

多态的好处就是,当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了,因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增Animal子类;

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:

                ┌───────────────┐
                │    object     │
                └───────────────┘
                        │
           ┌────────────┴────────────┐
           │                         │
           ▼                         ▼
    ┌─────────────┐           ┌─────────────┐
    │   Animal    │           │    Plant    │
    └─────────────┘           └─────────────┘
           │                         │
     ┌─────┴──────┐            ┌─────┴──────┐
     │            │            │            │
     ▼            ▼            ▼            ▼
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│   Dog   │  │   Cat   │  │  Tree   │  │ Flower  │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

静态语言 vs 动态语言

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了:

class Timer(object):
    def run(self):
        print('Start...')

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的

十九 实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

给实例绑定属性的方法是通过实例变量,或者通过self变量:

class Student(object):
    def __init__(self, name):
        self.name = name

s = Student('Bob')
s.score = 90

但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):
    name = 'Student'

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。来测试一下:

>>> class Student(object):
...     name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

二十 @property

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改:

s = Student()
s.score = 9999

这显然不合逻辑。为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数:

class Student(object):
    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

现在,对任意的Student实例进行操作,就不能随心所欲地设置score了:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。

有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!

还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):
    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.score(60)
>>> s.score # OK,实际转化为s.score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student(object):
    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。

要特别注意:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:

class Student(object):
    # 方法名称和实例变量均为birth:
    @property
    def birth(self):
        return self.birth

这是因为调用s.birth时,首先转换为方法调用,在执行return self.birth时,又视为访问self的属性,于是又转换为方法调用self.birth(),造成无限递归,最终导致栈溢出报错RecursionError

二十一 多进程

如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?

由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。

multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

执行结果如下:

Parent process 928.
Child process will start.
Run child process test (929)...
Process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。

join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

Pool

如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

执行结果如下:

Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.

代码解读:

Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

请注意输出的结果,task 0123是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:

p = Pool(5)

就可以同时跑5个进程。

由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。

子进程

很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。

subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。

下面的例子演示了如何在Python代码中运行命令nslookup www.python.org,这和命令行直接运行的效果是一样的:

import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

运行结果:

$ nslookup www.python.org
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
www.python.org	canonical name = python.map.fastly.net.
Name:	python.map.fastly.net
Address: 199.27.79.223

Exit code: 0

如果子进程还需要输入,则可以通过communicate()方法输入:

import subprocess

print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)

上面的代码相当于在命令行执行命令nslookup,然后手动输入:

set q=mx
python.org
exit

运行结果如下:

$ nslookup
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
python.org	mail exchanger = 50 mail.python.org.

Authoritative answers can be found from:
mail.python.org	internet address = 82.94.164.166
mail.python.org	has AAAA address 2001:888:2000:d::a6


Exit code: 0

进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

运行结果如下:

Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

Python优点

Python优点:

1、语法简单:Python 不要求在每个语句的最后写分号,当然写上也没错;定义变量时不需要指明类型,甚至可以给同一个变量赋值不同类型的数据。

2、面向对向免费开源的解释型语言:比如我们开发了一个 BBS 系统,放在互联网上让用户下载,那么用户下载到的就是该系统的所有源代码,并且可以随意修改。这也是解释型语言本身的特性,想要运行程序就必须有源代码,解释器和模块也是开源的。

3、强大的扩展性:具有脚本语言中最丰富和强大的类库,这些类库覆盖了文件 I/O、GUI、网络编程、数据库访问、文本操作等绝大部分应用场景。这些类库的底层代码不一定都是 Python,还有很多 C/C++ 的身影。当需要一段关键代码运行速度更快时,就可以使用 C/C++ 语言实现,然后在 Python 中调用它们。Python 能把其它语言“粘”在一起,所以被称为“胶水语言

4、Python 属于典型的解释型语言,所以运行 Python 程序需要解释器的支持,只要你在不同的平台安装了不同的解释器,你的代码就可以随时运行,不用担心任何兼容性问题,真正的“一次编写,到处运行”,几乎支持所有常见的平台

缺点:运行速度慢。