原文发表在我的博客主页,转载请注明出处!
建议二十八:区别对待可变对象和不可变对象
python中一切皆对象,每一个对象都有一个唯一的标识符(id())、类型(type())以及值,对象根据其值能否修改分为可变对象和不可变对象,其中数字、字符串、元组属于不可变对象,字典以及列表、字节数组属于可变对象。
来看一段程序:
class Student(object):
def __init__(self,name,course=[]):
self.name = name
self.course = course
def addcourse(self,coursename):
self.course.append(coursename)
def printcourse(self):
for item in self.course:
print item
xl = Student('xl')
xl.addcourse('computer')
xl.addcourse('automation')
print xl.name + "'s course:"
xl.printcourse()
print "~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
cyj = Student('cyj')
cyj.addcourse('software')
cyj.addcourse('NLP')
print cyj.name + "'s course:"
cyj.printcourse()
运行结果会让初学者大吃一惊:
xl's course:
computer
automation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cyj's course:
computer
automation
software
NLP
通过查看xl和cyj的course变量的id,发现他们的值是一样的,即指向内存中的同一块地址,但是xl和cyj却是两个不同的对象。在实例化这两个对象的时候,这两个对象被分配了不同的内存空间,并且调用init()函数进行初始化,但由于init()函数的第二个参数是个默认参数,默认参数在函数调用的时候仅仅被评估一次,以后都会使用第一次评估的结果,因此实际上对象空间里面course所指向的是list的地址,这时我们在将可变对象作为默认参数的时候要警惕的,对可变对象的更改会直接影响原对象,可以用如下方式解决:
def __init__(self,name,course=None):
self.name = name
if course is None:course = []
self.course = course
对于不可变对象来说,当我们对其进行相关操作的时候,python实际上仍然保存原来的值,重新创建一个新的对象。当有两个对象同时指向一个字符串对象的时候,对其中一个对象的操作并不会影响另一个对象。比如:
str1 = "write pythonic code"
str2 = str1
str1 = str1[-4:]
print id(str1)
print id(str2)
print str1
print str2
建议二十九:和{}:一致性容器初始化形式
列表是一个很有用的数据结构,由于其灵活性在实际应用中被广泛使用。对于列表来说,列表解析十分常用。
列表解析的语法如下,它迭代iterable中的每一个元素,当条件满足的时候便根据表达式expr计算的内容生成一个元素并放入新的列表中,依次类推,最终返回整个列表。
[expr for iter_item in iterable if cond_expr]
列表解析的使用非常灵活:
- 支持多重嵌套,如果需要生成一个二维列表可以使用列表解析嵌套的方式
nested_list = [['Hello', 'World'],['Goodbye', 'World']]
nested_list = [[ele.upper() for ele in word] for word in nested_list]
- 支持多重迭代
[(a,b) for a in ['1', '2', '3', '4'] for b in ['a', 'b', 'c', 'd'] if a != b]
- 表达式可以是简单表达式,也可以是复杂表达式,甚至是函数
def f(v):
if v%2 == 0:
v = v ** 2
else:
v = v + 1
return v
print [f(v) for v in [1,2,3,-1] if v > 0]
print [v ** 2 if v %2 == 0 else v + 1 for v in [1,2,3,-1] if v > 0]
- iterable可以是任意可迭代对象
fp = open('wdf.py','r')
res = [i for i in fp if 'weixin' in i]
print res
为什么要推荐在需要生成列表的时候使用列表解析呢?
- 使用列表解析更为直观清晰,代码更为简洁
- 列表解析的效率更高,但是对于大数据处理,列表解析并不是一个最佳选择,过多的内存消耗可能会导致MemoryError
除了列表可以使用列表解析的语法之外,其他内置的数据结构也支持,如下:
#generator
(expr for iter_item in iterable if cond_expr)
#set
{expr for iter_item in iterable if cond_expr}
#dict
{expr1: expr2 for iter_item in iterable if cond_expr}
建议三十:记住函数传参既不是传值也不是传引用
以往关于python中函数传参数有三种观点:
- 传值
- 传引用
- 可变对象传引用,不可变对象传值
这些理解都是有些偏差的,python中的赋值与我们所理解的C/C++等语言的赋值的意思并不一样。以如下语句为例来看C/C++和python是如何运作的
a = 5, b= a, b = 7
C/C++中当执行b=a的时候,在内存中申请一块内存并将a的值复制到该内存中,当执行b=7之后是将b对应的值从5修改到7
python中赋值并不是复制,b=a操作使得b与a引用同一对象,而b=7则是将b指向对象7
因此,对于python函数参数既不是传值也不是传引用,应该是传对象或者说传对象的引用。函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,调用者和被调用者之间共享这个对象,而对于不可变对象,由于并不能真正被修改,因此,修改往往是通过生成一个新对象然后赋值来实现的。
建议三十一:慎用变长参数
python支持可变长度的参数列表,可以通过在函数定义的时候使用args和**kwargs这两个特殊语法来实现。
使用args来实现可变参数列表:*args用于接收一个包装为元组形式的参数列表来传递非关键字参数,参数个数可以任意:
def sumf(*args):
res = 0
for x in args[:]:
res += x
return res
print sumf(2,3,4)
print sumf(1,2,3,4,5)
使用**kwargs接收字典形式的关键字参数列表,其中字典的键值分别表示不可变参数的参数名和值:
def category_table(**kwargs):
for name, value in kwargs.items():
print '{0} is a kind of {1}'.format(name, value)
category_table(apple = 'fruit', carrot = 'vegetable')
当普通参数,默认参数,和上述两种参数同时存在的时候,会优先给普通参数和默认参数赋值,为什么要慎用可变长参数呢?
- 使用过于灵活,是代码不够清晰
- 如果一个函数的参数列表很长,虽然可以通过使用*args和**kwargs来简化函数的定义,但通常意味着这个函数可以有更好的实现方式,应该被重构。
- 可变长参数适合在下列情况下使用:
- 为函数添加一个装饰器
- 如果参数的数目不确定,可以考虑使用变长参数
- 用来实现函数的多态或者在继承情况下子类需要调用父类的某些方法的时候
参数三十二:深入理解str()和repr()的区别
这两个方法都可以将python中的对象转换为字符串,他们的使用以及输出都非常相似,区别呢?
- 两者之间的目标不同:str()主要面向用户,其目的是可读性,返回形式为用户友好性和可读性都较强的字符串类型,而repr()面向python解释器,或者说开发人员,其目的是准确性,其返回值表示python解释器的内部函数,常用作debug
- 在解释器中直接输入a时默认调用repr()函数,而print a则调用str()函数
- repr()的返回值一般可以用eval()函数来还原
obj == eval(repr(obj))
- 这两个方法分别调用内建的__str__和__repr__()方法,一般来说在类中都应该定义后者,而前者方法则为可选,如果没有,默认使用后者的结果来返回对象的字符串表示形式
建议三十三:分清staticmethod和classmethod的使用场景
python中的静态方法(staticmethod)和类方法(classmethod)都依赖于装饰器来实现,用法如下:
#staticmethod
class C(object):
@staticmethod
def f(arg1, arg2, ...):
#classmethod
class C(object):
@classmethod
def f(arg1, arg2, ...):
静态方法和类方法都可以通过类名.方法名或者实例.方法名的形式来访问。其中静态方法没有常规方法的特殊行为,如绑定、非绑定、隐式参数等规则,而类方法的调用使用类本身作为其隐含参数,但调用本身并不需要显示提供该参数。
那为什么需要静态方法和类方法呢?假设有水果类Fruit,它用属性total表示总量,用set()来设置重量,print_total()方法来打印水果数量。类Apple和类Orange继承自Fruit,现需要分别跟踪不同类型的水果的总量,实现方法汇总:
- 利用普通的实例方法来实现:在Apple和Orange类中分别定义类变量total,然后覆盖基类的set()和print_total()方法
- 使用类方法实现
class Fruit(object):
total = 0
def print_total(cls):
print cls.total
@classmethod
def set(cls, value):
cls.total = value
class Apple(Fruit):
pass
class Orange(Fruit):
pass
app1 = Apple()
app1.set(200)
app2 = Apple()
org1 = Orange()
org1.set(300)
org2 = Orange()
app1.print_total()
org1.print_total()
简单分析可知,针对不同种类的水果对象调用set()方法的时候隐形传入的参数为该对象所对应的类,在调用set()的过程中动态生成了对应的类的类变量。
静态方法一般适用于既不跟特定的实例相关也不跟特定的类相关的方法。他存在于类中,较之外部函数能够更加有效的将代码组织起来,从而使相关代码的垂直距离更近,提高代码的可维护性。
参考:编写高质量代码--改善python程序的91个建议