他山之石,calling by share——python中既不是传址也不是传值

事情是这样的,Python里是传址还是传值令人疑惑,限于本人没有C基础,所以对大家的各类水平层次不一的解答难以确信。
第一个阶段:
在读《python基础教程第二版》的时候感到疑惑,然后群友解答(略敷衍),接着就是知乎上提问(感谢大家的热心回答,但我很晚才收到推送
虽然是某天早晨睡不着,翻看公众号的时候看见一篇《不要再问 "Python 函数中,参数是传值,还是传引用?" 这种没有意义的问题了》的文章,初步释疑惑(但后来我觉得他的说法虽然形象,但是不准确)
第二个阶段:
在阅读《JavaScript高级程序设计(第2版)》一书的P71遇到问题,书中说JS都是值传递,但举的例子我觉得没有很好地论证他的观点,于是最终得到发现了JS中其实是“call by share”
第三个阶段:
在读《python学习手册》P159,书里提到了共享引用(Shared References)的概念,果然之前JS的官方的说法是正确的。

赋值:
无论对不变类型变量、可变对象赋值
    对变量赋新值都会断开对原值的引用,而成为新值的引用
不可变类型
例子1
a = 1
b = a
a = 3
print(a,b)
# 3 1
可变类型
例子2
L1 = [1,2,3]
L2 = L1
L1 = 13
print(L1,L2)
# 13 [1, 2, 3]
L1 = ['a','b','c']
print(L1,L2)
# ['a', 'b', 'c'] [1, 2, 3]
    对其赋值,就是在原处修改

调用方法修改属性:
L1 = [1,2,3]
L2 = L1
L1[1] = 13
print(L1,L2)
# [1, 13, 3] [1, 13, 3]
L1 = ['a','b','c']
print(L1,L2)
# ['a', 'b', 'c'] [1, 13, 3]
为什么说是调用方法修改属性呢?因为运算符重载中告诉我们,其实"[]"索引其实是自动调用了__getitem__方法(P713)。
因为Python中给函数传递实参即是“形参名=实参”的形式,跟例子1中b=a、例子2中L2=L1的意思是一样的。
记住以上两条,那么即可自如地应对python中参数传递的问题:
代码1:
def testImmutable(arg):
arg = 2
print(arg)

a = 1
testImmutable(a)
# 2
print(a)
# 1
这个用"对变量赋新值都会断开对原值的引用,而成为新值的引用"解释即可,所以这样的函数无法修改不可变对象。
当然,可变对象也是不可以修改
代码2:
def testMutable(arg):
arg = ['a','b','c']
print(arg)

L = [1,2,3]
testMutable(L)
# ['a', 'b', 'c']
print(L)
# [1, 2, 3]
(这一点可以跟后面补充内容部分的JS的例子做对比)

但是我们可以用方法修改可变对象的属性
代码3:
def testAttr(args):
args.append(1.3)

a = [1.1,1.2]
print(a)
# [1.1, 1.2]
testAttr(a)
print(a)
# [1.1, 1.2, 1.3]

******************************♣******************************
P530提到了一个陷阱
也就是说我如果想要让形参默认为一个空列表,如果写成这样是有问题的:
def saver(x=[]): # Saves away a list object
x.append(1) # Changes same object each time!
print(x)
saver([2]) # Default not used
# [2, 1]
saver() # Default used
# [1]
saver() # Grows on each call!
# [1, 1]
saver()
# [1, 1, 1]
这是因为,这个空列表对象只是在def语句执行时被创建的,不会每次函数调用时都得到一个新的列表,所以每次新的元素加入后,列表会在原来的基础上变大,因为这个空列表在每次函数调用的时候都没有被重置
正确写法如下:
def saver(x=None):
if x is None: # No argument passed?
x = [] # Run code to make a new list each time
x.append(1) # Changes new list object
print(x)
saver([2])
# [2, 1]
saver() # Doesn't grow here
# [1]
saver()
# [1]
    
    ******************************♣******************************
如下代码也会报错
x = 11
def selector():
print(X)
X = 88
selector()
    这个错误原因有点类似于JS的变量声明提升
    
R中的闭包可参看Python和JS的,而Python中的作用域问题,可以参看JS的(比如没有块作用域、局部变量声明提升等等的问题)

补充
****************************♣****************************
在阅读《JavaScript高级程序设计(第2版)》一书的P71遇到问题:
      为什么都是值传递,而不是传递引用?

******************************♣******************************
引用1:
引用是C++中的概念,其操作符是: & ,这跟C中是取地址操作符一样,
但是意义不一样,C中没有引用的。 (切记 !)

******************************♣******************************
引用2:
传值,
是把实参的值赋值给行参
那么对行参的修改,不会影响实参的值

传地址
是传值的一种特殊方式,只是他传递的是地址,不是普通的如int
那么传地址以后,实参和行参都指向同一个对象

传引用
真正的以地址的方式传递参数
传递以后,行参和实参都是同一个对象只是他们名字不同而已
 例如有人名叫王小毛,他的绰号是三毛。说三毛怎么怎么的,其实就是对王小毛说三道四。

******************************♣******************************
引用3:
Java中只有传值
在JS和Java中,只有按值传递call by value,并没有按引用传递call by reference。引用类型的变量依然是按值传递的。这是因为引用类型也是一个变量,只是这个变量的值是另一块内存的地址。将引用类型变量传递给函数形参,函数同样会为该形参在栈上开辟新的空间,存放实参中的地址值因此这依然是按值传递,只不过这个值是地址值而已只有在C或C++里才有按引用传递。指的是用取地址符取到变量地址,作为实参传递给形参的指针变量。这里的实参是地址常量,不是指针变量。JS和Java里都没有取地址符,就只有按值传递。这一点在Core Java 第一卷里面有说明。不过,JS和Java里虽然没有对地址的直接操作,但是仍然有间接寻址的概念,即通过引用变量改变其地址所指向的内存区。

******************************♣******************************
引用4——在JAVA中:
基础类型变量存的是具体值,所以基础类型变量传值,传的是具体值的副本
引用类型变量存的就是地址值,所以引用类型变量传值,传的就是地址值了
——一切传引用其实本质上是传值
  1. int num = 10;
  2. String str = "hello";  //String是一个引用类型
  1. num = 20;
  2. str = "java";‘
 

第一个例子:基本类型
  1. void foo(int value) {
  2.     value = 100;
  3. }
  4. foo(num); // num 没有被改变
这很容易理解,因为我们传递的是值的副本
第二个例子:没有提供改变自身方法的引用类型
  1. void foo(String text) {
  2.     text = "windows";
  3. }
  4. foo(str); // str 也没有被改变
str存着地址0x11
而0x11地址被传递给形参text
而text = "windows"
是让text指向了一个新的对象,text中存了一个新地址0x12
第三个例子:提供了改变自身方法的引用类型
  1. StringBuilder sb = new StringBuilder("iphone");
  2. void foo(StringBuilder builder) {
  3.     builder.append("4");
  4. }
  5. foo(sb); // sb 被改变了,变成了"iphone4"。
sb是一个StringBuilder对象,他的地址0x21被传递给builder
此时,builder跟sb指向了同一个对象
所以使用append方法操作这个对象的时候,sb特被改变
第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
  1. StringBuilder sb = new StringBuilder("iphone");
  2. void foo(StringBuilder builder) {
  3.     builder = new StringBuilder("ipad");
  4. }
  5. foo(sb); // sb 没有被改变,还是 "iphone"。
builder尽管获得了builder的地址,但是没有使用,就被重新存了一个地址了
再看:
  1. public class TestMain {
  2. public static void main(String[] args) {
  3. List<Integer> list = new ArrayList<Integer>();
  4. for (int i = 0; i < 10; i++) {
  5. list.add(i);
  6. }
  7. add(list);
  8. for (Integer j : list) {
  9. System.err.print(j+",");;
  10. }
  11. System.err.println("");
  12. System.err.println("*********************");
  13. String a="A";
  14. append(a);
  15. System.err.println(a);
  16. int num = 5;
  17. addNum(num);
  18. System.err.println(num);
  19. }
  20. static void add(List<Integer> list){
  21. list.add(100);
  22. }
  23. static void append(String str){
  24. str+="is a";
  25. }
  26. static void addNum(int a){
  27. a=a+10;
  28. }
  29. }
打印出来的结果是:
0,1,2,3,4,5,6,7,8,9,100,
*********************
A                                        
5
尽管str是对象类型的,但是str+="is a";其实是str = str + "is a" 已经指向了一个新的对象了
而String类并没有提供改变自身的方法
他所谓的“改变”其实都是重新绑定到一个新的值

******************************♣******************************
引用5:
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。
  1. var obj = {x : 1};
  2. function foo(o) {
  3.     o = 100;
  4. }
  5. foo(obj);
  6. console.log(obj.x); // 仍然是1, obj并未被修改为100.
如果是按引用传递,修改形参o的值,应该影响到实参才对。但这里修改o的值并未影响obj。 因此JS中的对象并不是按引用传递。那么究竟对象的值在JS中如何传递的呢?
按共享传递 call by sharing
准确的说,JS中的基本类型按值传递,对象类型按共享传递(call by sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出。该求值策略被用于Python、Java、Ruby、JS等多种语言。

******************************♣******************************
引用6:
ECMAScript中对call by sharing的定义:
The main point of this strategy is that function receives the copy of the reference to object. This reference copy is associated with the formal parameter and is its value.
 Regardless the fact that the concept of the reference in this case appears, this strategy should not be treated as call by reference (though, in this case the majority makes a mistake), because the value of the argument is not the direct alias(别名), but the copy of the address.
 The main difference consists that :
1 assignment of a new value to argument inside the function does not affect object outside (as it would be in case of call by reference). 
   给参数赋一个新值,不会影响到外面的对象
2 However, because formal parameter(形参), having an address copy, gets access to the same object that is outside (i.e. the object from the outside completely was not copied as would be in case of call by value), 
changes of properties of local argument object — are reflected in the external object.
   改变局部参数对象的属性,会影响到外部对象

******************************♣******************************
所以啊,个人的总结就是:
其实所谓的引用都是值传递,差异出现在:
     基本类型(JavaScript、Java)/不可变类型(Python)
        传递值副本
     引用类型(JavaScript、Java)/可变类型(Python)
        传递地址——传递地址区别于传递引用
        解释成共享传递
        :在函数内给形参赋新值(无论是基本类型还是对象类型),不改变函数外的对象
        :在函数内修改形参的属性,会影响到函数外的对象






原文地址:https://www.cnblogs.com/xuanlvshu/p/6938610.html