سه اشتباه رایجی که هنگام کد زدن در پایتون باید مراقب آنها باشید

سه اشتباه رایجی که هنگام کد زدن در پایتون باید مراقب آنها باشید

اشتباه کردن در فرایند یادگیری یک زبان برنامه‌نویسی امری کاملاً بدیهی و رایج در میان دولوپرهای تازه‌کار است. در واقع، گاهی‌اوقات کسانی که قصد دارند یک زبان برنامه‌نویسی را فرا بگیرند، به دلیل عدم برخورداری از یک منتور یا منابع آموزشی مناسب، مسیر را اشتباه می‌روند. در همینن راستا، در این پست قصد داریم با سه مورد از اشتباهات متداولی که هنگام یادگیری زبان برنامه‌نویسی پایتون ممکن است برای هر تازه‌کاری به وجود بیاید صحبت کنیم (کما اینکه نگارندهٔ این پست هم بر اساس تصوراتی که از زبان‌های دیگر مثل جاوا، سی و ... داشته، در حین یادگیری زبان پایتون بعضاً ساعت‌ها وقت صرف کرده تا متوجه وجود چنین خطاهایی گردد!)

1. استفاده از متغیرهای Mutable به عنوان مقدار دیفالت یک آرگومان هنگام تعریف تابع
بایستی قبل از ورود به بحث، کمی دربارۀ انواع دیتا تایپ‌ها در پایتون صحبت کنیم تا منظور از دیتا تایپ Mutable کاملاً روشن شود. متغیرها در پایتون از نظر امکان تغییرپذیری بعد از تعریف، به دو دسته تقسیم می‌شوند که عبارتند از Mutable و Immutable.

Mutable: به متغیری گفته می‌شود که بعد از تعریف، امکان تغییر و Assign کردن مقدار جدید به آن وجود دارد. لیست، دیکشنری و سِت از دیتا تایپ‌های Mutable هستند.

Immutable: به متغیری گفته می‌شود که بعد از تعریف، امکان Assign کردن مقدار جدید به آن وجود ندارد. اعداد (Integer ،Float و Complex)، استرینگ، تاپل، بولین و اصطلاحاً FrozenSet، از دیتا تایپ‌های Immutable هستند.

حال برگردیم به اصل مطلب و برای اینکه بهتر متوجه موضوع شوید، بگذارید یک مثال بزنیم. فرض کنید تابعی به صورت زیر داریم:

def fn(var1, var2=[]):
    var2.append(var1)
    print var2

همان‌طور که می‌بینید، متغیر دوم آن به عنوان دیفالت یک لیست خالی (که از متغیرهای Mutable می‌باشد) تعریف شده است. هنگامی که تابع با مقادیر زیر فراخوانی می‌شود:

fn(3)
fn(4)
fn(5)

انتظار داریم که مقادیر زیر برگردانده شوند: 

[3]
[4]
[5]

ولی در کمال تعجب، با خروجی زیر مواجه خواهیم شد:

[3]
[3, 4]
[3, 4, 5]

بر خلاف تصوری که داریم، پایتون هر باری که تابع فراخوانی می‌شود، لیست را مجدداً نمی‌سازد و از همان لیست قبلی استفاده می‌کند. البته دقت داشته باشید که این اتفاق فقط زمانی رخ می‌دهد که هر سه شرط زیر برقرار باشند:
- تابع دارای مقدار دیفالت باشد.
- مقدار دیفالت تابع حتماً Mutable باشد.
- تابع با مقدار دیفالت فراخوانی شود.

مثلاً وقتی که تابع با مقادیر زیر فراخوانی شود:

fn(3, [4])

خروجی زیر را مشاهده خواهیم کرد:

[4, 3]

زیرا شرط سوم برقرار نبود یا مثلاً وقتی که تابع به شکل زیر است:

def func(message="my message"):
    print message

هنگام فراخوانی مشکلی ایجاد نمی‌شود زیرا مقدار دیفالت آن Immutable است. برای رفع مشکل مذکور، باید هنگام تعریف تابع از یک متغیر Immutable مثل None استفاده کنیم. مثلاً می‌توانیم تابع اولیه را به شکل زیر بازنویسی کنیم:

def fn(var1, var2=None):
    if not var2:
        var2 = []
    var2.append(var1)

با این تغییر، در واقع فرایند Instantiation (نمونه‌سازی و مقداردهی متغیر) آرگومان‌های تابع از زمان تعریف به زمان فراخوانی منتقل می‌شوند و هنگامی که تابع با آرگومان‌های زیر فراخوانی می‌شود:

fn(3)
fn(4)
fn(5)

مقادیر مورد انتظارمان را خواهیم دید:

[3]
[4]
[5]

2. استفاده از متغیرهای Mutable به عنوان خصوصیت‌های یک کلاس
لازم به توضیح است که Property به متغیرهایی گفته می‌شود داخل یک کلاس تعریف می‌شوند و به عبارتی مختص همان کلاس می‌باشند؛ و اما پراپرتی‌ها می‌توانند متعلق به کلاس و یا آبجکت باشند (در مورد تفاوت بین کلاس و آبجکت باید بگویم که آبجکت نمونۀ ساخته شدۀ یک کلاس می‌باشد). در پایتون پراپرتی‌های کلاس قبل از متدها تعریف می‌شوند:

class cls1(object):
    a = []

    def method(self, b):
        self.a.append(b)

ولی پراپرتی‌هایی که متعلق به یک آبجکت هستند، داخل متد __init__ (یا همان Constructor) تعریف می‌شوند و البته قبل از آنها حتماً کلیدواژهٔ Self قرار داده می‌شود:

class cls2(object):
    def __init__(self):
        self.a = []

    def method(self, b):
        self.a.append(b)

حال که تفاوت پراپرتی‌های یک کلاس و یک آبجکت را متوجه شدیم، برمی‌گردیم به اصل موضوع و با یک مثال مطلب را شروع می‌کنیم. فرض کنید کلاس زیر را تعریف کرده‌ایم:

class URLCatcher(object):
    urls = []

    def add_url(self, url):
        self.urls.append(url)

حال از این کلاس دو آبجکت تحت عناوین a و b می‌سازیم:

a = URLCatcher()
a.add_url('http://www.google.')
b = URLCatcher()
b.add_url('http://www.bbc.co.')

وقتی که متغیر urls را در این دو آبجکت بررسی می‌کنیم، با کمال تعجب مشاهده می‌کنیم که: 

b.urls
['http://www.google.com', 'http://www.bbc.co.uk']

a.urls
['http://www.google.com', 'http://www.bbc.co.uk']

در واقع همان‌طور که مشاهده می‌شود، مقدار متغیر urls که انتظار داشتیم هنگام Instant (نمونه) گرفتن از کلاس هر بار تعریف شود و هر آبجکت مقدار مختص خودش را داشته باشد، بین دو آبجکت مشترک شده و مقادیر قبلی را در خود نگاه داشته است و دلیل آن (همانند مورد قبل)، ایجاد متغیر در زمان تعریف کلاس است و نه در زمانی که آبجکت ساخته می‌شود و برای اصلاح آن می‌توانیم کلاس را به صورت زیر ریفکتور کنیم:

class URLCatcher(object):
    def __init__(self):
        self.urls = []

    def add_url(self, url):
        self.urls.append(url)

حال هر آبجکت متغیر مخصوص به خود را دارا است و هنگام فراخوانی پراپرتی‌ url، دیگر با مشکل بالا مواجه نخواهیم شد. 

3. Assignment در متغیرهای Mutable
طبق معمول برای اینکه ذهن شما کمی درگیر شود، اشتباه رایج سوم را نیز با ذکر یک مثال شروع می‌کنیم:

a = {'1': "one", '2': 'two'}
b = a
b['3'] = 'three'

همان‌طور که می‌بینید، یک دیکشنری (که Mutable است) را تعریف کرده و آن را در دیگری کپی کرده ایم (با استفاده از علامت =) و سپس مقدار یکی را تغییر داده‌ایم؛ حال خروجی را با هم مشاهده می‌کنیم:

a
{'1': "one", '2': 'two', '3': 'three'}

b
{'1': "one", '2': 'two', '3': 'three'}

همان‌طور که می‌بینید، هر دو دیکشنری تغییر کرده‌اند در صورتی که ما فقط یکی را تغییر داده‌ایم. حال اجازه دهید برای اینکه ماجرا جالب‌تر شود، مثال دیگری را بررسی کنیم:

c = (2, 3)
d = c
d = (4, 5)

دقیقاً همان کار بالا را انجام داده‌ایم تنها تفاوت آن است که در اینجا از تاپل (که Immutable است) استفاده کرده‌ایم؛ حال خروجی را مشاهده می‌کنیم:

c
(2, 3)

d
(4, 5)

این بار برخلاف مورد قبلی، متغیر اول تغییری نکرده است و این به دلیل عملکردهای متفاوت اپراتور = در پایتون می‌باشد. اپراتور = در پایتون گاهی به معنای Assignment (مقداردهی) و گاهی هم به معنای Define (تعریف) و Assignment هم‌زمان بوده و در نهایت هنگامی که دو متغیر را در دو سمت آن قرار می‌دهیم، به معنای Shallow Copy است (در Shallow Copy دو متغیر از هم جدا نیستند و هر دو به یک نقطه از حافظه اشاره می‌کنند). تفکیک حالت‌های اول و دوم کاملاً بر مبنای Mutable یا Immutable بودن متغیری است که تعریف می‌کنیم. مثلاً هنگامی که می‌نویسیم:

a = 1
l = ['d', 'e']
t = (3, 4)

در اینجا = هم Define می‌کند و هم Assignment انجام می‌دهد ولی هنگامی که می‌نویسیم:

l[1] = 'f'

اپراتور = به معنای Assignment خالی است و عملیات Define صورت نمی‌گیرد؛ و البته این حالت را فقط در متغیرهای Mutable که تغییرپذیر هستند می‌توان مشاهده کرد. حال با توجه به این توضیحات، یک بار دیگر مثال اول را بررسی می‌کنیم:

a = {'1': "one", '2': 'two'}
b = a
b['3'] = 'three'

در این مثال، به دلیل Mutable بودن آبجکت‌ها هنگامی که خواستیم یکی از اعضای متغیر را تغییر دهیم، در واقع علامت = در قالب Assignment ظاهر شده و مقدار درون آبجکت را (بدون آنکه آن را از نو بسازد) تغییر داده و متغیرهای a و b یک Shallow Copy از یکدیگر می‌باشند؛ بنابراین Assignment مقادیر هر دو را تغییر می‌دهد اما در مثال دوم:

c = (2, 3)
d = c
d = (4, 5)

باز هم a و b یک Shallow Copy از یکدیگر بوده و هر دو به یک نقطه از حافظه اشاره می‌کنند؛ اما آخرین = به دلیل آنکه یک متغیر Immutable داریم، به معنای Define و Assignment هم‌زمان تلقی شده و یک آبجکت جدید به اسم b می‌سازد و بنابراین مقدار a را تغییر نمی‌دهد. حال برای اینکه بتوانیم مشکل کپی را در متغیرهای Mutable حل کنیم، می‌توانیم از هر کدام از حالت‌های زیر استفاده کنیم:

b = a[:]
b = a.copy()

از حالت اول فقط برای لیست‌ها استفاده می‌کنند و از دومی برای همۀ متغیرهای Mutable می‌توان استفاده کرد و اصطلاحاً یک Deep Copy از متغیر اولی می‌سازد (Deep Copy در واقع برخلاف Shallow Copy، یک متغیر جدید در یک بخش جدید از حافظه می‌سازد که فقط به لحاظ ساختار به متغیر اول شباهت دارد).

مواردی که در بالا به آنها اشاره شد، بخشی از مشکلاتی هستند که ممکن است برای یک دولوپر تازه‌کار در دنیای پایتون اتفاق بیفتند. چنانچه موارد دیگری به ذهن شما رسید، خوشحال می‌شویم که در بخش نظرات همین مقاله با ما و سایر کاربران سکان آکادمی در میان بگذارید. همچنین اگر علاقمند به شروع یادگیری زبان پایتون از ابتدا هستید، می‌توانید به دورهٔ آموزش رایگان زبان برنامه‌نویسی پایتون در سکان آکادمی مراجعه نمایید.

منبع


محمد طاهری