编程常见问题

您所在的位置:网站首页 脚本源码是什么意思 编程常见问题

编程常见问题

#编程常见问题 | 来源: 网络整理| 查看: 265

语言核心内容¶ 变量明明有值,为什么还会出现 UnboundLocalError?¶

It can be a surprise to get the UnboundLocalError in previously working code when it is modified by adding an assignment statement somewhere in the body of a function.

以下代码:

>>> x = 10 >>> def bar(): ... print(x) ... >>> bar() 10

正常工作,但是以下代码

>>> x = 10 >>> def foo(): ... print(x) ... x += 1

results in an UnboundLocalError:

>>> foo() Traceback (most recent call last): ... UnboundLocalError: local variable 'x' referenced before assignment

原因就是,当对某作用域内的变量进行赋值时,该变量将成为该作用域内的局部变量,并覆盖外部作用域中的同名变量。由于 foo 的最后一条语句为 x 分配了一个新值,编译器会将其识别为局部变量。因此,前面的 print(x) 试图输出未初始化的局部变量,就会引发错误。

在上面的示例中,可以将外部作用域的变量声明为全局变量以便访问:

>>> x = 10 >>> def foobar(): ... global x ... print(x) ... x += 1 ... >>> foobar() 10

与类和实例变量貌似但不一样,其实以上是在修改外部作用域的变量值,为了提示这一点,这里需要显式声明一下。

>>> print(x) 11

你可以使用 nonlocal 关键字在嵌套作用域中执行类似的操作:

>>> def foo(): ... x = 10 ... def bar(): ... nonlocal x ... print(x) ... x += 1 ... bar() ... print(x) ... >>> foo() 10 11 Python 的局部变量和全局变量有哪些规则?¶

函数内部只作引用的 Python 变量隐式视为全局变量。如果在函数内部任何位置为变量赋值,则除非明确声明为全局变量,否则均将其视为局部变量。

起初尽管有点令人惊讶,不过考虑片刻即可释然。一方面,已分配的变量要求加上 global 可以防止意外的副作用发生。另一方面,如果所有全局引用都要加上 global ,那处处都得用上 global 了。那么每次对内置函数或导入模块中的组件进行引用时,都得声明为全局变量。这种杂乱会破坏 global 声明用于警示副作用的有效性。

为什么在循环中定义的参数各异的 lambda 都返回相同的结果?¶

假设用 for 循环来定义几个取值各异的 lambda(即便是普通函数也一样):

>>> squares = [] >>> for x in range(5): ... squares.append(lambda: x**2)

以上会得到一个包含5个 lambda 函数的列表,这些函数将计算 x**2。大家或许期望,调用这些函数会分别返回 0 、1 、 4 、 9 和 16。然而,真的试过就会发现,他们都会返回 16 :

>>> squares[2]() 16 >>> squares[4]() 16

这是因为 x 不是 lambda 函数的内部变量,而是定义于外部作用域中的,并且 x 是在调用 lambda 时访问的——而不是在定义时访问。循环结束时 x 的值是 4 ,所以此时所有的函数都将返回 4**2 ,即 16 。通过改变 x 的值并查看 lambda 的结果变化,也可以验证这一点。

>>> x = 8 >>> squares[2]() 64

为了避免发生上述情况,需要将值保存在 lambda 局部变量,以使其不依赖于全局 x 的值:

>>> squares = [] >>> for x in range(5): ... squares.append(lambda n=x: n**2)

以上 n=x 创建了一个新的 lambda 本地变量 n,并在定义 lambda 时计算其值,使其与循环当前时点的 x 值相同。这意味着 n 的值在第 1 个 lambda 中为 0 ,在第 2 个 lambda 中为 1 ,在第 3 个中为 2,依此类推。因此现在每个 lambda 都会返回正确结果:

>>> squares[2]() 4 >>> squares[4]() 16

请注意,上述表现并不是 lambda 所特有的,常规的函数也同样适用。

如何跨模块共享全局变量?¶

在单个程序中跨模块共享信息的规范方法是创建一个特殊模块(通常称为 config 或 cfg)。只需在应用程序的所有模块中导入该 config 模块;然后该模块就可当作全局名称使用了。因为每个模块只有一个实例,所以对该模块对象所做的任何更改将会在所有地方得以体现。 例如:

config.py:

x = 0 # Default value of the 'x' configuration setting

mod.py:

import config config.x = 1

main.py:

import config import mod print(config.x)

Note that using a module is also the basis for implementing the singleton design pattern, for the same reason.

导入模块的“最佳实践”是什么?¶

通常请勿使用 from modulename import * 。因为这会扰乱 importer 的命名空间,且会造成未定义名称更难以被 Linter 检查出来。

请在代码文件的首部就导入模块。这样代码所需的模块就一目了然了,也不用考虑模块名是否在作用域内的问题。每行导入一个模块则增删起来会比较容易,每行导入多个模块则更节省屏幕空间。

按如下顺序导入模块就是一种好做法:

standard library modules -- e.g. sys, os, argparse, re

third-party library modules (anything installed in Python's site-packages directory) -- e.g. dateutil, requests, PIL.Image

locally developed modules

为了避免循环导入引发的问题,有时需要将模块导入语句移入函数或类的内部。Gordon McMillan 的说法如下:

当两个模块都采用 "import " 的导入形式时,循环导入是没有问题的。但如果第 2 个模块想从第 1 个模块中取出一个名称("from module import name")并且导入处于代码的最顶层,那导入就会失败。原因是第 1 个模块中的名称还不可用,这时第 1 个模块正忙于导入第 2 个模块呢。

如果只是在一个函数中用到第 2 个模块,那这时将导入语句移入该函数内部即可。当调用到导入语句时,第 1 个模块将已经完成初始化,第 2 个模块就可以进行导入了。

如果某些模块是平台相关的,可能还需要把导入语句移出最顶级代码。这种情况下,甚至有可能无法导入文件首部的所有模块。于是在对应的平台相关代码中导入正确的模块,就是一种不错的选择。

只有为了避免循环导入问题,或有必要减少模块初始化时间时,才把导入语句移入类似函数定义内部的局部作用域。如果根据程序的执行方式,许多导入操作不是必需的,那么这种技术尤其有用。如果模块仅在某个函数中用到,可能还要将导入操作移入该函数内部。请注意,因为模块有一次初始化过程,所以第一次加载模块的代价可能会比较高,但多次加载几乎没有什么花费,代价只是进行几次字典检索而已。即使模块名超出了作用域,模块在 sys.modules 中也是可用的。

为什么对象之间会共享默认值?¶

新手程序员常常中招这类 Bug。请看以下函数:

def foo(mydict={}): # Danger: shared reference to one dict for all calls ... compute something ... mydict[key] = value return mydict

第一次调用此函数时, mydict 中只有一个数据项。第二次调用 mydict 则会包含两个数据项,因为 foo() 开始执行时, mydict 中已经带有一个数据项了。

大家往往希望,函数调用会为默认值创建新的对象。但事实并非如此。默认值只会在函数定义时创建一次。如果对象发生改变,就如上例中的字典那样,则后续调用该函数时将会引用这个改动的对象。

按照定义,不可变对象改动起来是安全的,诸如数字、字符串、元组和 None 之类。而可变对象的改动则可能引起困惑,例如字典、列表和类实例等。

因此,不把可变对象用作默认值是一种良好的编程做法。而应采用 None 作为默认值,然后在函数中检查参数是否为 None 并新建列表、字典或其他对象。例如,代码不应如下所示:

def foo(mydict={}): ...

而应这么写:

def foo(mydict=None): if mydict is None: mydict = {} # create a new dict for local namespace

参数默认值的特性有时会很有用处。 如果有个函数的计算过程会比较耗时,有一种常见技巧是将每次函数调用的参数和结果缓存起来,并在同样的值被再次请求时返回缓存的值。这种技巧被称为“memoize”,实现代码可如下所示:

# Callers can only provide two parameters and optionally pass _cache by keyword def expensive(arg1, arg2, *, _cache={}): if (arg1, arg2) in _cache: return _cache[(arg1, arg2)] # Calculate the value result = ... expensive computation ... _cache[(arg1, arg2)] = result # Store result in the cache return result

也可以不用参数默认值来实现,而是采用全局的字典变量;这取决于个人偏好。

如何将可选参数或关键字参数从一个函数传递到另一个函数?¶

请利用函数参数列表中的标识符 * 和 ** 归集实参;结果会是元组形式的位置实参和字典形式的关键字实参。然后就可利用 * 和 ** 在调用其他函数时传入这些实参:

def f(x, *args, **kwargs): ... kwargs['width'] = '14.3c' ... g(x, *args, **kwargs) 形参和实参之间有什么区别?¶

Parameters are defined by the names that appear in a function definition, whereas arguments are the values actually passed to a function when calling it. Parameters define what kind of arguments a function can accept. For example, given the function definition:

def func(foo, bar=None, **kwargs): pass

foo 、 bar 和 kwargs 是 func 的形参。 不过在调用 func 时,例如:

func(42, bar=314, extra=somevar)

42 、 314 和 somevar 则是实参。

为什么修改列表 'y' 也会更改列表 'x'?¶

如果代码编写如下:

>>> x = [] >>> y = x >>> y.append(10) >>> y [10] >>> x [10]

或许大家很想知道,为什么在 y 中添加一个元素时, x 也会改变。

产生这种结果有两个因素:

变量只是指向对象的一个名称。执行 y = x 并不会创建列表的副本——而只是创建了一个新变量 y,并指向 x 所指的同一对象。这就意味着只存在一个列表对象,x 和 y 都是对它的引用。

列表属于 mutable 对象,这意味着它的内容是可以修改的。

在调用 append() 之后,该可变对象的内容由 [] 变为 [10]。由于 x 和 y 这两个变量引用了同一对象,因此用其中任意一个名称所访问到的都是修改后的值 [10]。

如果把赋给 x 的对象换成一个不可变对象:

>>> x = 5 # ints are immutable >>> y = x >>> x = x + 1 # 5 can't be mutated, we are creating a new object here >>> x 6 >>> y 5

可见这时 x 和 y 就不再相等了。因为整数是 immutable 对象,在执行 x = x + 1 时,并不会修改整数对象 5,给它加上 1;而是创建了一个新的对象(整数对象 6 )并将其赋给 x (也就是改变了 x 所指向的对象)。在赋值完成后,就有了两个对象(整数对象 6 和 5 )和分别指向他俩的两个变量( x 现在指向 6 而 y 仍然指向 5 )。

Some operations (for example y.append(10) and y.sort()) mutate the object, whereas superficially similar operations (for example y = y + [10] and sorted(y)) create a new object. In general in Python (and in all cases in the standard library) a method that mutates an object will return None to help avoid getting the two types of operations confused. So if you mistakenly write y.sort() thinking it will give you a sorted copy of y, you'll instead end up with None, which will likely cause your program to generate an easily diagnosed error.

不过还存在一类操作,用不同的类型执行相同的操作有时会发生不同的行为:即增量赋值运算符。例如,+= 会修改列表,但不会修改元组或整数(a_list += [1, 2, 3] 与 a_list.extend([1, 2, 3]) 同样都会改变 a_list,而 some_tuple += (1, 2, 3) 和 some_int += 1 则会创建新的对象)。

换而言之:

对于一个可变对象( list 、 dict 、 set 等等),可以利用某些特定的操作进行修改,所有引用它的变量都会反映出改动情况。

对于一个不可变对象( str 、 int 、 tuple 等),所有引用它的变量都会给出相同的值,但所有改变其值的操作都将返回一个新的对象。

如要知道两个变量是否指向同一个对象,可以利用 is 运算符或内置函数 id()。

如何编写带有输出参数的函数(按照引用调用)?¶

请记住,Python 中的实参是通过赋值传递的。由于赋值只是创建了对象的引用,所以调用方和被调用方的参数名都不存在别名,本质上也就不存在按引用调用的方式。通过以下几种方式,可以得到所需的效果。

返回一个元组:

>>> def func1(a, b): ... a = 'new-value' # a and b are local names ... b = b + 1 # assigned to new objects ... return a, b # return new values ... >>> x, y = 'old-value', 99 >>> func1(x, y) ('new-value', 100)

这差不多是最明晰的解决方案了。

使用全局变量。这不是线程安全的方案,不推荐使用。

传递一个可变(即可原地修改的) 对象:

>>> def func2(a): ... a[0] = 'new-value' # 'a' references a mutable list ... a[1] = a[1] + 1 # changes a shared object ... >>> args = ['old-value', 99] >>> func2(args) >>> args ['new-value', 100]

传入一个接收可变对象的字典:

>>> def func3(args): ... args['a'] = 'new-value' # args is a mutable dictionary ... args['b'] = args['b'] + 1 # change it in-place ... >>> args = {'a': 'old-value', 'b': 99} >>> func3(args) >>> args {'a': 'new-value', 'b': 100}

或者把值用类实例封装起来:

>>> class Namespace: ... def __init__(self, /, **args): ... for key, value in args.items(): ... setattr(self, key, value) ... >>> def func4(args): ... args.a = 'new-value' # args is a mutable Namespace ... args.b = args.b + 1 # change object in-place ... >>> args = Namespace(a='old-value', b=99) >>> func4(args) >>> vars(args) {'a': 'new-value', 'b': 100}

没有什么理由要把问题搞得这么复杂。

最佳选择就是返回一个包含多个结果值的元组。

如何在 Python 中创建高阶函数?¶

有两种选择:嵌套作用域、可调用对象。假定需要定义 linear(a,b) ,其返回结果是一个计算出 a*x+b 的函数 f(x)。 采用嵌套作用域的方案如下:

def linear(a, b): def result(x): return a * x + b return result

或者可采用可调用对象:

class linear: def __init__(self, a, b): self.a, self.b = a, b def __call__(self, x): return self.a * x + self.b

采用这两种方案时:

taxes = linear(0.3, 2)

都会得到一个可调用对象,可实现 taxes(10e6) == 0.3 * 10e6 + 2 。

可调用对象的方案有个缺点,就是速度稍慢且生成的代码略长。不过值得注意的是,同一组可调用对象能够通过继承来共享签名(类声明):

class exponential(linear): # __init__ inherited def __call__(self, x): return self.a * (x ** self.b)

对象可以为多个方法的运行状态进行封装:

class counter: value = 0 def set(self, x): self.value = x def up(self): self.value = self.value + 1 def down(self): self.value = self.value - 1 count = counter() inc, dec, reset = count.up, count.down, count.set

以上 inc() 、 dec() 和 reset() 的表现,就如同共享了同一计数变量一样。

如何复制 Python 对象?¶

一般情况下,用 copy.copy() 或 copy.deepcopy() 基本就可以了。并不是所有对象都支持复制,但多数是可以的。

某些对象可以用更简便的方法进行复制。比如字典对象就提供了 copy() 方法:

newdict = olddict.copy()

序列可以用切片操作进行复制:

new_l = l[:] 如何找到对象的方法或属性?¶

For an instance x of a user-defined class, dir(x) returns an alphabetized list of the names containing the instance attributes and methods and attributes defined by its class.

如何用代码获取对象的名称?¶

一般而言这是无法实现的,因为对象并不存在真正的名称。赋值本质上是把某个名称绑定到某个值上;def 和 class 语句同样如此,只是值换成了某个可调用对象。比如以下代码:

>>> class A: ... pass ... >>> B = A >>> a = B() >>> b = a >>> print(b) >>> print(a)

Arguably the class has a name: even though it is bound to two names and invoked through the name B the created instance is still reported as an instance of class A. However, it is impossible to say whether the instance's name is a or b, since both names are bound to the same value.

代码一般没有必要去“知晓”某个值的名称。通常这种需求预示着还是改变方案为好,除非真的是要编写内审程序。

在 comp.lang.python 中,Fredrik Lundh 在回答这样的问题时曾经给出过一个绝佳的类比:

这就像要知道家门口的那只猫的名字一样:猫(对象)自己不会说出它的名字,它根本就不在乎自己叫什么——所以唯一方法就是问一遍你所有的邻居(命名空间),这是不是他们家的猫(对象)……

……并且如果你发现它有很多名字或根本没有名字,那也不必惊讶!

逗号运算符的优先级是什么?¶

逗号不是 Python 的运算符。 请看以下例子:

>>> "a" in "b", "a" (False, 'a')

由于逗号不是运算符,而只是表达式之间的分隔符,因此上述代码就相当于:

("a" in "b"), "a"

而不是:

"a" in ("b", "a")

对于各种赋值运算符( = 、 += 等)来说同样如此。他们并不是真正的运算符,而只是赋值语句中的语法分隔符。

是否提供等价于 C 语言 "?:" 三目运算符的东西?¶

有的。语法如下:

[on_true] if [expression] else [on_false] x, y = 50, 25 small = x if x 1 else 1: f(x,f), range(10)))) # Mandelbrot set print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+'\n'+y,map(lambda y, Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM, Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro, i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k=4.0) or 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr( 64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy),range(Sy ))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24)) # \___ ___/ \___ ___/ | | |__ lines on screen # V V | |______ columns on screen # | | |__________ maximum of "iterations" # | |_________________ range on y axis # |____________________________ range on x axis

请不要在家里尝试,骚年!

函数形参列表中的斜杠(/)是什么意思?¶

A slash in the argument list of a function denotes that the parameters prior to it are positional-only. Positional-only parameters are the ones without an externally usable name. Upon calling a function that accepts positional-only parameters, arguments are mapped to parameters based solely on their position. For example, divmod() is a function that accepts positional-only parameters. Its documentation looks like this:

>>> help(divmod) Help on built-in function divmod in module builtins: divmod(x, y, /) Return the tuple (x//y, x%y). Invariant: div*y + mod == x.

形参列表尾部的斜杠说明,两个形参都是仅限位置形参。因此,用关键字参数调用 divmod() 将会引发错误:

>>> divmod(x=3, y=4) Traceback (most recent call last): File "", line 1, in TypeError: divmod() takes no keyword arguments


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3