پایتون '!=' 'نیست' نیست: مقایسه آبجکت ها در پایتون


تفاوت ظریفی بین عملگر هویت پایتون (is) و عملگر برابری (==) وجود دارد. کد شما می تواند زمانی که از عملگر Python is برای مقایسه اعداد استفاده می کنید، خوب اجرا شود، تا زمانی که ناگهان این کار را نمی کند. ممکن است در جایی شنیده باشید که عملگر پایتون سریعتر از عملگر == است، یا ممکن است احساس کنید که بیشتر پایتونی به نظر می رسد. با این حال، بسیار مهم است که به خاطر داشته باشید که این اپراتورها کاملا یکسان رفتار نمی کنند.

عملگر == مقدار یا برابری دو شی را مقایسه می کند، در حالی که عملگر پایتون is بررسی می کند که آیا دو متغیر به یک شی در حافظه اشاره می کنند یا خیر. در اکثریت قریب به اتفاق موارد، این بدان معناست که شما باید از عملگرهای برابری == و != استفاده کنید، مگر زمانی که با هیچ کدام مقایسه می کنید.

در این آموزش یاد خواهید گرفت:

  • تفاوت بین برابری ابژه و هویت چیست؟
  • چه زمانی از عملگرهای برابری و هویت برای مقایسه آبجکت ها استفاده کنیم؟
  • کاری که این عملگرهای پایتون در زیر کاپوت انجام می دهند
  • چرا استفاده از مقادیر است و نیست که منجر به رفتار غیرمنتظره می شود؟
  • چگونه یک متد کلاس __eq__() سفارشی برای تعریف رفتار عملگر برابری بنویسیم؟

مقایسه هویت با عملگرهای پایتون is and not

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

می توانید از id() برای بررسی هویت یک شی استفاده کنید:

>>> help(id)
Help on built-in function id in module builtins:

id(obj, /)
    Return the identity of an object.

    This is guaranteed to be unique among simultaneously existing objects.
    (CPython uses the object's memory address.)

>>> id(id)
2570892442576

آخرین خط آدرس حافظه را نشان می دهد که خود شناسه تابع داخلی در آن ذخیره می شود.

برخی موارد رایج وجود دارد که اشیاء با مقدار یکسان به طور پیش فرض شناسه یکسانی دارند. به عنوان مثال، اعداد -5 تا 256 در CPython کارآموزی می شوند. هر عدد در یک مکان منفرد و ثابت در حافظه ذخیره می شود که حافظه را برای اعداد صحیح رایج ذخیره می کند.

می توانید از sys.intern() برای کارآموزی رشته ها برای اجرا استفاده کنید. این عملکرد به شما امکان می دهد آدرس های حافظه آنها را به جای مقایسه رشته ها کاراکتر به کاراکتر مقایسه کنید:

>>> from sys import intern
>>> a = 'hello world'
>>> b = 'hello world'
>>> a is b
False
>>> id(a)
1603648396784
>>> id(b)
1603648426160

>>> a = intern(a)
>>> b = intern(b)
>>> a is b
True
>>> id(a)
1603648396784
>>> id(b)
1603648396784

متغیرهای a و b در ابتدا به دو شی مختلف در حافظه اشاره می کنند، همانطور که شناسه های مختلف آنها نشان داده شده است. هنگامی که آنها را کارآموزی می کنید، اطمینان حاصل می کنید که a و b به یک شی در حافظه اشاره می کنند. هر رشته جدیدی با مقدار "hello world" اکنون در یک مکان حافظه جدید ایجاد می شود، اما هنگامی که این رشته جدید را کارآموزی می کنید، مطمئن می شوید که به همان آدرس حافظه اولین "Hello world" که کارآموزی کرده اید اشاره می کند.

سایر اشیاء که به طور پیش فرض کارآموزی می شوند عبارتند از: None ،True ،False و رشته های ساده. به خاطر داشته باشید که بیشتر اوقات، اشیاء مختلف با مقدار یکسان در آدرس های حافظه جداگانه ذخیره می شوند. این بدان معناست که شما نباید از عملگر Python is برای مقایسه مقادیر استفاده کنید.

زمانی که فقط برخی از اعداد صحیح کارآموزی می شوند

در پشت صحنه، پایتون اشیاء را با مقادیر رایج (به عنوان مثال، اعداد صحیح -5 تا 256) برای ذخیره حافظه کارآموزی می کند. بیت کد زیر به شما نشان می دهد که چگونه فقط برخی از اعداد صحیح دارای یک آدرس حافظه ثابت هستند:

>>> a = 256
>>> b = 256
>>> a is b
True
>>> id(a)
1638894624
>>> id(b)
1638894624

>>> a = 257
>>> b = 257
>>> a is b
False

>>> id(a)
2570926051952
>>> id(b)
2570926051984

در ابتدا، a و b به یک شی کارآموز در حافظه اشاره می کنند، اما زمانی که مقادیر آنها خارج از محدوده اعداد صحیح رایج (از -5 تا 256) باشد، در آدرس های حافظه جداگانه ذخیره می شوند.

وقتی چندین متغیر به یک شی اشاره می کنند

هنگامی که از عملگر تخصیص (=) استفاده می کنید تا یک متغیر را با دیگری برابر کنید، این متغیرها را به همان شی در حافظه نشان می دهید. این ممکن است منجر به رفتار غیرمنتظره برای اشیاء قابل تغییر شود:

>>> a = [1, 2, 3]
>>> b = a
>>> a
[1, 2, 3]
>>> b
[1, 2, 3]

>>> a.append(4)
>>> a
[1, 2, 3, 4]
>>> b
[1, 2, 3, 4]

>>> id(a)
2570926056520
>>> id(b)
2570926056520

چه اتفاقی افتاده است؟ شما یک عنصر جدید به a اضافه می کنید، اما اکنون b نیز شامل این عنصر است! خوب، در خطی که b=a است، b را طوری تنظیم می کنید که به همان آدرس حافظه a اشاره کند، به طوری که هر دو متغیر اکنون به یک شی اشاره می کنند.

اگر این لیست ها را مستقل از یکدیگر تعریف کنید، در آدرس های حافظه مختلف ذخیره می شوند و به طور مستقل رفتار می کنند:

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False
>>> id(a)
2356388925576
>>> id(b)
2356388952648

از آنجایی که a و b اکنون به اشیاء مختلف در حافظه اشاره می کنند، تغییر یکی بر دیگری تأثیر نمی گذارد.

مقایسه برابری با عملگرهای پایتون == و !=

به یاد بیاورید که اشیاء با مقدار یکسان اغلب در آدرس های حافظه جداگانه ذخیره می شوند. اگر می خواهید بررسی کنید که آیا دو شی مقدار یکسانی دارند یا خیر، صرف نظر از اینکه در کجا در حافظه ذخیره می شوند، از عملگرهای برابری == و != استفاده کنید. در اکثریت قریب به اتفاق موارد، این همان کاری است که شما می خواهید انجام دهید.

هنگامی که کپی آبجکت برابر است اما یکسان نیست

در مثال زیر، b را به عنوان یک کپی از a (که یک شی قابل تغییر است، مانند یک لیست یا یک فرهنگ لغت) تنظیم کرده اید. هر دو متغیر مقدار یکسانی خواهند داشت، اما هر کدام در آدرس حافظه متفاوتی ذخیره می شوند:

>>> a = [1, 2, 3]
>>> b = a.copy()
>>> a
[1, 2, 3]
>>> b
[1, 2, 3]

>>> a == b
True
>>> a is b
False

>>> id(a)
2570926058312
>>> id(b)
2570926057736

a و b اکنون در آدرس های حافظه مختلف ذخیره می شوند، بنابراین A is b دیگر True را برنمی گرداند. با این حال، a == b True را برمی گرداند زیرا هر دو شی مقدار یکسانی دارند.

مقایسه بر اساس برابری چگونه کار می کند

جادوی عملگر برابری == در متد کلاس __eq__() شی سمت چپ علامت == اتفاق می افتد.

این یک متد کلاس جادویی است که هر زمان که نمونه ای از این کلاس با شی دیگری مقایسه شود، فراخوانی می شود. اگر این روش اجرا نشود، == آدرس های حافظه دو شی را به طور پیش فرض مقایسه می کند.

به عنوان یک تمرین، یک کلاس SillyString بسازید که از str به ارث می برد و __eq__() را پیاده سازی کنید تا مقایسه کنید که آیا طول این رشته با طول آبجکت دیگر یکسان است یا خیر:

class SillyString(str):
    # This method gets called when using == on the object
    def __eq__(self, other):
        print(f'comparing {self} to {other}')
        # Return True if self and other have the same length
        return len(self) == len(other)    

اکنون، یک "hello world" SillyString باید برابر با سیم "world hello" و حتی با هر شی دیگری با همان طول باشد:

>>> # Compare two strings
>>> 'hello world' == 'world hello'
False

>>> # Compare a string with a SillyString
>>> 'hello world' == SillyString('world hello')
comparing world hello to hello world
True

>>> # Compare a SillyString with a list
>>> SillyString('hello world') == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
comparing hello world to [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
True

البته این رفتار احمقانه ای برای یک شی است که در غیر این صورت به عنوان یک رشته رفتار می کند، اما نشان می دهد که وقتی دو شی را با استفاده از == مقایسه می کنید، چه اتفاقی می افتد. عملگر != پاسخ معکوس این را می دهد مگر اینکه یک روش کلاس __ne__() خاص پیاده سازی شود.

مثال بالا همچنین به وضوح به شما نشان می دهد که چرا استفاده از عملگر پایتون is برای مقایسه با None به جای عملگر == تمرین خوبی است. نه تنها سریعتر است زیرا آدرس های حافظه را مقایسه می کند، بلکه ایمن تر است زیرا به منطق هیچ متدهای کلاس __eq__() بستگی ندارد.

مقایسه عملگرهای مقایسه پایتون

به عنوان یک قاعده کلی، همیشه باید از عملگرهای برابری == و != استفاده کنید، مگر زمانی که با هیچ کدام مقایسه می کنید:

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

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

متغیرهایی با مقدار یکسان اغلب در آدرس های حافظه جداگانه ذخیره می شوند. این بدان معنی است که شما باید از == و != برای مقایسه مقادیر آنها استفاده کنید و از عملگرهای پایتون is and not not فقط زمانی استفاده کنید که بخواهید بررسی کنید که آیا دو متغیر به آدرس حافظه یکسانی اشاره می کنند یا خیر.

نتیجه

در این آموزش، شما یاد گرفتید که == و != مقدار دو شی را مقایسه می کنند، در حالی که پایتون عملگرها را مقایسه می کنند که آیا دو متغیر به یک شی در حافظه اشاره می کنند یا خیر. اگر این تمایز را در نظر داشته باشید، باید بتوانید از رفتار غیرمنتظره در کد خود جلوگیری کنید.

اگر می خواهید در مورد دنیای شگفت انگیز کارآموزی شی و عملگر پایتون بیشتر بخوانید، بررسی کنید چرا تقریبا هرگز نباید از "is" در پایتون استفاده کنید. همچنین می توانید نگاهی به نحوه استفاده از sys.intern() برای بهینه سازی استفاده از حافظه و زمان مقایسه رشته ها بیندازید، اگرچه این احتمال وجود دارد که پایتون قبلا به طور خودکار این کار را در پشت صحنه شما انجام دهد.

اکنون که یاد گرفتید عملگرهای برابری و هویت در زیر کاپوت چه می کنند، می توانید روش های کلاس __eq__() خود را بنویسید، که نحوه مقایسه نمونه های این کلاس را هنگام استفاده از عملگر == تعریف می کند. بروید و دانش جدید خود را در مورد این عملگرهای مقایسه پایتون به کار ببرید!