اشتباه کردن در فرایند یادگیری یک زبان برنامهنویسی امری کاملاً بدیهی در میان دولوپرهای تازهکار است به طوری که گاهی اوقات کسانی که قصد دارند یک زبان برنامهنویسی را فرا بگیرند، به دلیل عدم برخورداری از یک منتور یا منابع آموزشی مناسب، مسیر را اشتباه میروند که در همین راستا در این پست قصد داریم با سه مورد از اشتباهات متداولی که هنگام یادگیری زبان برنامهنویسی پایتون ممکن است برای هر تازهکاری به وجود بیاید، صحبت کنیم.
استفاده از متغیرهای 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]
استفاده از متغیرهای Mutable به عنوان پراپرتیهای یک کلاس
لازم به توضیح است که Property به متغیرهایی گفته میشود داخل یک کلاس تعریف میشوند و به عبارتی مختص همان کلاس میباشند اما در عین حال پراپرتیها هم میتوانند متعلق به کلاس و هم متعلق به یک آبجکت باشند (در مورد تفاوت بین کلاس و آبجکت باید بگویم که آبجکت نمونۀ ساختهشد از روی یک کلاس است.) در پایتون پراپرتیهای کلاس قبل از متدها تعریف میشوند:
class cls1(object):
a = []
def method(self, b):
self.a.append(b)
ولی پراپرتیهایی که متعلق به یک آبجکت هستند، داخل متد __init__ یا همان کانستراکتور تعریف میشوند و البته قبل از آنها حتماً کلیدواژهٔ 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 که انتظار داشتیم هنگام آبجکت ساختن از روی کلاس هر بار تعریف شود و هر آبجکت مقدار مختص به خود را داشته باشد، بین دو آبجکت مشترک شده و مقادیر قبلی را در خود نگاه داشته است و دلیلش هم همانند مورد قبل، ایجاد متغیر در زمان تعریف کلاس است و نه در زمانی که آبجکت ساخته میشود و برای رفع این مشکل میتوانیم کلاس خود را به صورت زیر ریفکتور کنیم:
class URLCatcher(object):
def __init__(self):
self.urls = []
def add_url(self, url):
self.urls.append(url)
از این پس، هر آبجکت متغیر مخصوص به خود را دارا است و هنگام فراخوانی پراپرتی url، دیگر با مشکلی مواجه نخواهیم شد.
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)
باز هم c و d یک Shallow Copy از یکدیگر بوده و هر دو به یک نقطه از حافظه اشاره میکنند اما آخرین = به دلیل اینکه یک متغیر Immutable داریم، به معنای Define و Assignment به صورت همزمان تلقی شده و یک آبجکت جدید به اسم d میسازد و بنابراین مقدار c را تغییر نمیدهد. حال برای اینکه بتوانیم این مشکل را در متغیرهای Mutable حل کنیم، میتوانیم از هر کدام از حالتهای زیر استفاده کنیم:
b = a[:]
یا مورد زیر:
b = a.copy()
از حالت اول فقط برای لیستها استفاده میکنند و از دومی برای همۀ متغیرهای Mutable میتوان استفاده کرد و اصطلاحاً یک Deep Copy از متغیر اولی میسازد (Deep Copy در واقع بر خلاف Shallow Copy، یک متغیر جدید در یک بخش جدید از حافظه میسازد که فقط به لحاظ ساختار به متغیر اول شباهت دارد.)
مواردی که در بالا به آنها اشاره شد بخشی از مشکلاتی هستند که ممکن است برای یک دولوپر تازهکار در حین یادگیری زبان برنامهنویسی پایتون اتفاق بیفتند اما در صورتی که موارد دیگری به ذهن شما میرسد، در بخش نظرات همین مقاله میتوانید با سایر کاربران سکان آکادمی در میان بگذارید. همچنین اگر علاقمند به شروع یادگیری زبان پایتون از ابتدا هستید، میتوانید به دورهٔ آموزش رایگان زبان برنامهنویسی پایتون در سکان آکادمی مراجعه نمایید.