大数跨境
0
0

From bug to gain:浅谈perl和python中浅拷贝和深拷贝的概念

From bug to gain:浅谈perl和python中浅拷贝和深拷贝的概念 Dr.X的基因空间
2025-05-14
0
导读:从bug中学习经验,从经验中获取知识

从bug到收获:浅谈编程语言的深拷贝与浅拷贝概念

写在前面的

       前几天在测试一个借助AI新写的程序中发现了一个bug,为了解决这个bug,我花了2个多小时判断bug,理解bug并解决bug。在AI流行的当下,如果编程者彻底丧失对代码的基础理解力,那么面对AI写的程序时很有可能忽视程序内部潜在的致命bug。从而产生错误的数据分析结果,影响项目的正常推进。因此无论是自我coding还是借助AI coding,把握每一次编程的机会判断、检查程序是否存在bug对于提升个人的底层编程能力和思维具有重要意义。接下来我会简单分享我在这次debug过程中发现的bug,并通过解决它的过程重新学习到的以前遗忘的知识。

my %endecs;
my %ennorm;
my %denorm;
open(FILE, "<", $sequence_file)
while(<FILE>){
    chomp;
    my @line = split/\t/;
    my $region = join("_", (@line[($#line - 5)..($#line - 3)]));
    my ($linker_mark, $domain_mark) = ($line[$#line - 2], $line[$#line - 1]);
    my $protein_motif = $line[$motif_type{$mtf_type}];

    if(($linker_mark * $domain_mark == -1and ($linker_mark + $domain_mark == 0)){
        if(($line[5] eq "+" and $linker_mark == 1or ($line[5] eq "-" and $domain_mark == 1)){
            $endecs{$region}{"CBD"}{$protein_motif} += 1;
            $endecs{$region}{"CBD"}{"total"} += 1;
            $endecs{"all-data"}{"CBD"}{$protein_motif} += 1;
            $endecs{"all-data"}{"CBD"}{"total"} += 1;
        }
        elsif(($line[5] eq "+" and $linker_mark == -1or ($line[5] eq "-" and $domain_mark == -1)){
            $endecs{$region}{"CD"}{$protein_motif} += 1;
            $endecs{$region}{"CD"}{"total"} += 1;
            $endecs{"all-data"}{"CD"}{$protein_motif} += 1;
            $endecs{"all-data"}{"CD"}{"total"} += 1;
        }
    }
    elsif(($linker_mark * $domain_mark == 0and ($linker_mark + $domain_mark == 1)){
        if(($line[5] eq "+" and $linker_mark == 1or ($line[5] eq "-" and $domain_mark == 1)){
            $ennorm{$region}{"CBD"}{$protein_motif} += 1;
            $ennorm{$region}{"CBD"}{"total"} += 1;
            $ennorm{"all-data"}{"CBD"}{$protein_motif} += 1;
            $ennorm{"all-data"}{"CBD"}{"total"} += 1;
        }
        elsif(($line[5] eq "+" and $linker_mark == 0or ($line[5] eq "-" and $domain_mark == 0)){
            $ennorm{$region}{"linker"}{$protein_motif} += 1;
            $ennorm{$region}{"linker"}{"total"} += 1;
            $ennorm{"all-data"}{"linker"}{$protein_motif} += 1;
            $ennorm{"all-data"}{"linker"}{"total"} += 1;
        }
    }
    elsif(($linker_mark * $domain_mark == 0and ($linker_mark + $domain_mark == -1)){
        if(($line[5] eq "+" and $linker_mark == 0or ($line[5] eq "-" and $domain_mark == 0)){
            $denorm{$region}{"linker"}{$protein_motif} += 1;
            $denorm{$region}{"linker"}{"total"} += 1;
            $denorm{"all-data"}{"linker"}{$protein_motif} += 1;
            $denorm{"all-data"}{"linker"}{"total"} += 1;
        }
        elsif(($line[5] eq "+" and $linker_mark == -1or ($line[5] eq "-" and $domain_mark == -1)){
            $denorm{$region}{"CD"}{$protein_motif} += 1;
            $denorm{$region}{"CD"}{"total"} += 1;
            $denorm{"all-data"}{"CD"}{$protein_motif} += 1;
            $denorm{"all-data"}{"CD"}{"total"} += 1;
        }
    }

}
close FILE;

my %endecs_count = %endecs;
my %ennorm_count = %ennorm;
my %denorm_count = %denorm;

       上面的代码是我用perl语言处理一批序列数据时写的程序。在这个程序中,我先定义了3个哈希用于储存序列的多个特征,在完成了全部数据读取后,我将记录了序列全部信息的3个哈希复制给了另外3个新定义的哈希。在后续的程序中我将先对3个旧哈希按照一个方向进行计算处理,再对另外3个新哈希按照另外一个方向进行计算处理。两种计算处理方法不相同。但是在我测试程序的过程中,我发现先处理完旧哈希后,新哈希的值会在旧哈希的处理过程中连带着被运算。当然,通过debug的过程我知道了原因,这是因为的定义的哈希拥有多层键,当我使用等号的方式复制旧哈希给新哈希时,新旧哈希的第一层键被复制,但是第二层和第三层键及其对应的值被两个哈希共享。所以无论接下来对哪个哈希采取何种计算,都会影响另外一个哈希。造成这个原因的bug就在等号上,通过等号的方式复制哈希本质上是进行了一次浅拷贝(Shallow copy),如果要让两个哈希独立计算其键值且互不影响则要使用深拷贝(Deep copy)。所以正确的复制方法应该是

my %endecs_count = %{dclone(\%endecs)};
my %ennorm_count = %{dclone(\%ennorm)};
my %denorm_count = %{dclone(\%denorm)};

何为浅拷贝?何为深拷贝?

浅拷贝

定义:浅拷贝只复制哈希的顶层键值对。如果哈希的值是引用(例如数组或另一个哈希),则复制的只是引用本身,而不是引用的底层数据。因此,修改新哈希中引用的数据会影响原哈希。
特点:简单且快速,仅复制顶层数据。嵌套数据结构(如数组或哈希)的引用保持共享。
实现方法: 浅拷贝可以通过直接赋值或使用 %{} 解引用来实现。

my %original = (a => 1b => [23], c => {x => 4});
my %shallow_copy = %original;  # 浅拷贝

# 修改浅拷贝中的数组
push @{$shallow_copy{b}}, 999;
print "@{$original{b}}\n";  # 输出: 2 3 999(原哈希也被修改)

深拷贝

定义:深拷贝会递归地复制哈希及其所有嵌套数据结构,生成完全独立的副本。修改新哈希的任何部分都不会影响原哈希。
特点:复制整个数据结构,包括嵌套的数组、哈希等。更耗费内存和计算资源。
实现方法:Perl 中没有内置的深拷贝函数,但可以使用Storable模块的dclone函数来实现深拷贝。

use Storable qw(dclone);

my %original = (a => 1b => [23], c => {x => 4});
my %deep_copy = %{ dclone(\%original) };  # 深拷贝

# 修改深拷贝中的数组
push @{$deep_copy{b}}, 999;
print "@{$original{b}}\n";  # 输出: 2 3(原哈希未受影响)

深拷贝和浅拷贝对比

特性
浅拷贝
深拷贝
复制内容
仅顶层键值对,引用共享
整个数据结构,包括嵌套数据
修改影响
修改嵌套数据影响原哈希
修改不影响原哈希
性能
快速,内存占用少
较慢,内存占用多
适用场景
简单数据或不需要修改嵌套
需要完全独立的副本

浅拷贝在python中的实现

       由于目前python成为了生物信息数据分析的主流编程语言,perl和python本质上是相通的,在perl上发现的问题可以迁移到python中进一步触类旁通。

  • 使用内置方法或模块:
import copy
original = {'a'1'b': [23], 'c': {'x'4}}
shallow_copy = original.copy()  # 浅拷贝
shallow_copy['b'].append(999)
print(original['b'])  # 输出: [23999](原字典被修改)

深拷贝在python中的实现

  • 使用 copy 模块的 deepcopy() 函数:
import copy
original = {'a'1'b': [23], 'c': {'x'4}}
deep_copy = copy.deepcopy(original)  # 深拷贝
deep_copy['b'].append(999)
print(original['b'])  # 输出: [23](原字典未受影响)

python和perl浅拷贝和深拷贝的异同

特性
Python
Perl
特性
Python
Perl
浅拷贝工具
dict.copy()
copy.copy(), 切片/推导式
直接赋值 %new = %original
深拷贝工具
copy.deepcopy() Storable::dclone
模块依赖
copy
(标准库,无需安装)
Storable
(通常核心模块)
语法简洁性
显式方法(如 copy())更直观
赋值简洁,但深拷贝需解引用
性能
显式方法(如 copy())更直观
Storable
(通常核心模块)
适用场景
浅拷贝内置,深拷贝需 copy 模块
浅拷贝内置,深拷贝需 Storable

为什么会有浅拷贝和深拷贝的区别

       其实关于深拷贝和浅拷贝的bug,我曾在读博期间写python程序的时候遇到过。但是那会儿只是解决了bug没有去思考为什么会有深浅拷贝的区别。比如,我个人认为,平时复制数据结构时,默认是复制的数据和原始数据互不影响,因此完全使用深拷贝就可以了,为什么要出现浅拷贝的概念呢?当时没有详细了解相关的背景,因此没有对以前的bug加深印象。这次写perl程序的时候第二次发现了这个bug,因此有必要思考为什么在perl或者python等这些编程语言中复制哈希/列表/字典时有这两种概念。关于回答这个问题,就要回到计算机编程语言发展的历史中了。

浅拷贝提出的背景

       浅拷贝的概念最早与支持复杂数据结构(如数组、结构体、对象或哈希)的编程语言相关,尤其是在 20 世纪 60-80 年代,编程语言(如 C、Lisp、Perl、Python 的前身等)开始广泛支持动态内存分配和引用机制。以下是推动浅拷贝概念提出的几个关键背景:

1.引用语义的兴起

背景:

  • 在早期语言(如 Fortran 或 Algol),数据结构多为简单标量或固定大小的数组,复制通常是值的直接复制(相当于深拷贝,但数据简单,成本低)。
  • 随着语言(如 Lisp、C、Smalltalk)引入动态内存分配和复杂数据结构(如链表、树、结构体),引用(或指针)成为管理内存的常见方式。引用允许数据结构通过内存地址共享,而不是每次都复制整个数据。

浅拷贝的起源:

  • 引用语义意味着复制一个数据结构时,默认只复制其引用(地址),而不是底层数据。这种行为天然就是“浅拷贝”,因为它只涉及顶层结构(如指针或引用),不递归复制嵌套数据。
  • 例如,在 C 中,复制一个结构体指针只复制指针本身,指向的内存保持共享。这种行为高效且符合内存管理的底层逻辑。

原因:

  • 浅拷贝是引用语义的直接体现,符合硬件和操作系统对内存管理的实现方式。
  • 它允许语言以最小成本支持复杂数据结构的复制,满足早期计算资源有限的环境需求。
2.性能和资源限制

背景:

  • 在 60-70 年代,计算机的内存和 CPU 资源非常有限。例如,早期计算机可能只有几 KB 的内存,处理大型数据结构需要极高的效率。
  • 复制复杂数据结构(如嵌套数组或链表)的全部内容(深拷贝)会消耗大量时间和内存,特别是在递归复制嵌套结构时。

浅拷贝的提出:

  • 浅拷贝作为一种低成本的复制方式被提出,只复制顶层结构(如数组的指针或结构体的字段),而不递归复制嵌套数据。
  • 例如,Lisp 中的列表操作或 C 中的指针赋值本质上是浅拷贝,允许快速创建数据副本。

原因:

  • 浅拷贝显著降低了复制的性能开销,适合资源受限的环境。
  • 它允许程序员在需要时手动管理嵌套数据的复制,提供了灵活性。
3. 编程需求的多样性

背景:

  • 随着编程语言应用于更广泛的场景(如系统编程、脚本开发、数据处理),程序员需要处理不同类型的复制需求。
  • 某些场景需要共享嵌套数据(如共享配置、缓存或全局状态),而其他场景需要完全独立的副本。

浅拷贝的角色:

  • 浅拷贝满足了共享嵌套数据的场景。例如,多个数据结构可能需要引用同一块内存以节省空间或保持一致性。
  • 它允许程序员显式控制哪些数据需要复制,哪些可以共享,增加了语言的表达能力。

为什么不默认深拷贝?

在语言设计之初,浅拷贝被优先考虑,而深拷贝作为次要选项,原因如下:

1.性能优先:

  • 早期计算机资源有限,深拷贝的高成本不适合作为默认行为。
  • 浅拷贝的低开销使其成为更实用的选择。

2.引用模型的逻辑:

  • 引用语义是复杂数据结构管理的核心,浅拷贝直接复制引用,符合语言的底层设计。
  • 深拷贝需要额外的递归逻辑,增加了实现复杂性。

3.共享需求的普遍性:

  • 许多程序设计依赖共享嵌套数据(如全局状态、缓存),浅拷贝天然支持这种模式。
  • 深拷贝会破坏共享,限制编程灵活性。

4.程序员控制:

  • 语言设计者倾向于让程序员决定是否需要深拷贝,而不是强制昂贵的操作。
  • 提供浅拷贝作为默认,深拷贝通过库或手动实现,平衡了灵活性和效率。


【声明】内容源于网络
0
0
Dr.X的基因空间
【中国科学院博士】10年生命科学数据挖掘研究经验,关注生物医药领域体外诊断(IVD)方向,如肿瘤早筛、传染病未知病原快速检测中的技术创新及其与人工智能(AI)的赋能应用
内容 176
粉丝 0
Dr.X的基因空间 【中国科学院博士】10年生命科学数据挖掘研究经验,关注生物医药领域体外诊断(IVD)方向,如肿瘤早筛、传染病未知病原快速检测中的技术创新及其与人工智能(AI)的赋能应用
总阅读0
粉丝0
内容176