অভিধান বনাম অবজেক্ট - যা আরও দক্ষ এবং কেন?


126

মেমরির ব্যবহার এবং সিপিইউ গ্রাহ্য - অভিধান বা অবজেক্টের ক্ষেত্রে পাইথনে আরও দক্ষ কী?

পটভূমি: পাইথনে আমাকে প্রচুর পরিমাণে ডেটা লোড করতে হবে। আমি একটি অবজেক্ট তৈরি করেছি যা কেবলমাত্র একটি ক্ষেত্রের ধারক। 4 এম দৃষ্টান্ত তৈরি করে এগুলি একটি অভিধানে রেখে প্রায় 10 মিনিট এবং 6 গিগাবাইট মেমরি নিয়েছিল। অভিধান প্রস্তুত হওয়ার পরে এটি অ্যাক্সেস করা চোখের পলক হয়।

উদাহরণ: পারফরম্যান্স যাচাই করতে আমি দুটি সাধারণ প্রোগ্রাম লিখেছিলাম যা একই কাজ করে - একটি হ'ল অবজেক্টস, অন্য অভিধান:

অবজেক্ট (ফাঁসির সময় ~ 18 সেকেন্ড):

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

অভিধান (মৃত্যুদন্ড কার্যকর সময় sec 12 সেকেন্ড):

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

প্রশ্ন: আমি কি কিছু ভুল করছি বা অভিধানের তুলনায় অবজেক্টের থেকেও দ্রুত? প্রকৃতপক্ষে অভিধান যদি আরও ভাল সম্পাদন করে তবে কেউ কেন ব্যাখ্যা করতে পারেন?


10
এর মতো বৃহত্তর সিকোয়েন্স তৈরি করার সময় আপনার অবশ্যই সত্যিকারের রেঞ্জের পরিবর্তে xrange ব্যবহার করা উচিত। অবশ্যই, আপনি যেহেতু মৃত্যুদণ্ড কার্যকর হওয়ার কয়েক সেকেন্ডের সাথে কাজ করছেন, এটি কোনও তাত্পর্যপূর্ণ করবে না, তবুও এটি একটি ভাল অভ্যাস।
জিয়াং চিয়ামিভ

2
অজগর 3 না হলে
বার্নি

উত্তর:


157

আপনি ব্যবহার চেষ্টা করেছেন __slots__?

ডকুমেন্টেশন থেকে :

ডিফল্টরূপে, পুরানো এবং নতুন-স্টাইলের উভয় শ্রেণীর উদাহরণগুলির মধ্যে অ্যাট্রিবিউট স্টোরেজের জন্য একটি অভিধান রয়েছে। এটি খুব কম উদাহরণ ভেরিয়েবলযুক্ত বস্তুর জন্য স্থান নষ্ট করে। বিপুল সংখ্যক উদাহরণ তৈরি করার সময় স্থান ব্যবহার তীব্র হয়ে উঠতে পারে।

__slots__নতুন ধাঁচের শ্রেণির সংজ্ঞা সংজ্ঞায়িত করে ডিফল্টটিকে ওভাররাইড করা যায় । __slots__ঘোষণা প্রতিটি পরিবর্তনশীল জন্য একটি মান রাখা প্রতিটি ইনস্ট্যান্সের মধ্যে উদাহরণস্বরূপ ভেরিয়েবল এবং মজুদ মাত্র যথেষ্ট স্থান একটি ক্রম লাগে। স্থানটি সংরক্ষণ করা হয়েছে কারণ __dict__প্রতিটি উদাহরণের জন্য তৈরি করা হয়নি।

সুতরাং এই স্মৃতি পাশাপাশি সময় বাঁচায়?

আমার কম্পিউটারে তিনটি পদ্ধতির তুলনা করা:

test_slots.py:

class Obj(object):
  __slots__ = ('i', 'l')
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_obj.py:

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

test_dict.py:

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

টেস্ট_নামটুপল.পি (২.6 এ সমর্থিত):

import collections

Obj = collections.namedtuple('Obj', 'i l')

all = {}
for i in range(1000000):
  all[i] = Obj(i, [])

বেঞ্চমার্ক চালান (সিপিথন 2.5 ব্যবহার করে):

$ lshw | grep product | head -n 1
          product: Intel(R) Pentium(R) M processor 1.60GHz
$ python --version
Python 2.5
$ time python test_obj.py && time python test_dict.py && time python test_slots.py 

real    0m27.398s (using 'normal' object)
real    0m16.747s (using __dict__)
real    0m11.777s (using __slots__)

সিপিথন ২.6.২ ব্যবহার করে, নামের টিউপল টেস্ট সহ:

$ python --version
Python 2.6.2
$ time python test_obj.py && time python test_dict.py && time python test_slots.py && time python test_namedtuple.py 

real    0m27.197s (using 'normal' object)
real    0m17.657s (using __dict__)
real    0m12.249s (using __slots__)
real    0m12.262s (using namedtuple)

সুতরাং হ্যাঁ (সত্যিই অবাক হওয়ার কিছু নেই), ব্যবহার __slots__করা একটি পারফরম্যান্স অপটিমাইজেশন। একটি নামকরণ tuple ব্যবহার অনুরূপ কর্মক্ষমতা আছে __slots__


2
এটা দুর্দান্ত - ধন্যবাদ! আমি আমার মেশিনেও এটি চেষ্টা করেছি - স্লট সহ বস্তুটি সবচেয়ে কার্যকর পদ্ধতির (আমি ~ 7 সেকস পেয়েছি)।
টকোকোসকা্কা

6
স্লট সহ বস্তুর জন্য ক্লাস ফ্যাক্টরি নামেও রয়েছে টিপলস, ডকস.পাইথন.আর । এটি স্পষ্টতই কম এবং সম্ভবত আরও অনুকূলিত।
জোচেন রিটজেল

আমি পাশাপাশি নামের টিপলগুলিও পরীক্ষা করেছি এবং ফলাফলগুলি সহ উত্তরটি আপডেট করেছি।
কোডেপ

1
আমি আপনার কোডটি কয়েকবার চালিয়েছি এবং আমার ফলাফলগুলি থেকে পৃথক হয়ে অবাক হয়েছি - স্লট = 3 সেকশন ওজেক্ট = 11 সেকেন্ড = 12 সেকেন্ড নামটুপল = 16 সেকেন্ড। আমি উইন 64৪ বিবিটিতে সিপিথন ২.6..6 ব্যবহার করছি
জোনাথন

Punchline জোরালো করতে - namedtuple পেয়েছিলাম খারাপ পরিবর্তে ফলাফল সেরা
জনাথন

15

কোনও অবজেক্টে অ্যাট্রিবিউট অ্যাক্সেস পর্দার পিছনে অভিধান অ্যাক্সেস ব্যবহার করে - সুতরাং অ্যাট্রিবিউট অ্যাক্সেস ব্যবহার করে আপনি অতিরিক্ত ওভারহেড যুক্ত করছেন। বস্তুর ক্ষেত্রে, অতিরিক্ত অতিরিক্ত মেমরি বরাদ্দ এবং কোড এক্সিকিউশন (যেমন __init__পদ্ধতিটির) কারণে আপনি অতিরিক্ত ওভারহেড ব্যয় করছেন ।

আপনার কোড, যদি oএকটি হয় Objউদাহরণস্বরূপ, o.attrসমতূল্য o.__dict__['attr']অতিরিক্ত ওভারহেড অল্প পরিমাণ সঙ্গে।


আপনি এটি পরীক্ষা করেছেন? o.__dict__["attr"]অতিরিক্ত ওভারহেডযুক্ত এক, একটি অতিরিক্ত বাইকোড ওপ গ্রহণ; obj.attr দ্রুত হয়। (অবশ্যই অ্যাট্রিবিউট অ্যাক্সেস সাবস্ক্রিপশন অ্যাক্সেসের চেয়ে ধীর হবে না - এটি একটি সমালোচনামূলক, ভারি অনুকূলিতকরণের কোড পাথ।)
গ্লেন মেইনার্ড ২

2
একথাও ঠিক যে যদি আপনি আসলে কি ণ .__ অভি __ [ "ATTR"] এটা ধীর হবে - আমি শুধু বলতে চাই যে এটা যে সমতূল্য ছিল না যে এটা যে পথ ঠিক বাস্তবায়ন করা হয় বোঝানো। আমার ধারণা এটি আমার কথা থেকে পরিষ্কার নয়। আমি অন্যান্য কারণগুলির যেমন মেমরি বরাদ্দ, কনস্ট্রাক্টর কল টাইম ইত্যাদিও উল্লেখ করেছি
বিনয় সজিপ ২

11 বছর পরেও কি আজও পাইথন 3 এর সাম্প্রতিক সংস্করণগুলির ক্ষেত্রে এটি রয়েছে?
ম্যাটানস্টার

9

আপনি কি নামধারী ব্যবহারের কথা বিবেচনা করেছেন ? ( অজগর জন্য লিঙ্ক 2.4 / 2.5 )

এটি কাঠামোগত উপাত্ত উপস্থাপনের নতুন স্ট্যান্ডার্ড উপায় যা আপনাকে একটি শ্রেণির কার্যকারিতা এবং শ্রেণীর সুবিধার্থে দেয়।

অভিধানের তুলনায় এটি কেবলমাত্র খারাপ দিক (টিউপসগুলির মতো) এটি আপনাকে সৃষ্টির পরে গুণাবলী পরিবর্তন করার ক্ষমতা দেয় না।


5

পাইথন ৩.6.১ এর জন্য @ হাগড্রাবাউন উত্তরের একটি অনুলিপি এখানে দেওয়া হয়েছে, আমি গণনাটি 5x আরও বড় করে দিয়েছি এবং প্রতিটি রান শেষে পাইথন প্রক্রিয়াটির মেমরির পদচিহ্ন পরীক্ষা করতে কিছু কোড যুক্ত করেছি।

ডাউনভোটারদের এটির আগে, পরামর্শ দেওয়া উচিত যে বস্তুর আকার গণনা করার এই পদ্ধতিটি সঠিক নয়।

from datetime import datetime
import os
import psutil

process = psutil.Process(os.getpid())


ITER_COUNT = 1000 * 1000 * 5

RESULT=None

def makeL(i):
    # Use this line to negate the effect of the strings on the test 
    # return "Python is smart and will only create one string with this line"

    # Use this if you want to see the difference with 5 million unique strings
    return "This is a sample string %s" % i

def timeit(method):
    def timed(*args, **kw):
        global RESULT
        s = datetime.now()
        RESULT = method(*args, **kw)
        e = datetime.now()

        sizeMb = process.memory_info().rss / 1024 / 1024
        sizeMbStr = "{0:,}".format(round(sizeMb, 2))

        print('Time Taken = %s, \t%s, \tSize = %s' % (e - s, method.__name__, sizeMbStr))

    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = makeL(i)

from collections import namedtuple
NT = namedtuple("NT", ["i", 'l'])

@timeit
def profile_dict_of_nt():
    return [NT(i=i, l=makeL(i)) for i in range(ITER_COUNT)]

@timeit
def profile_list_of_nt():
    return dict((i, NT(i=i, l=makeL(i))) for i in range(ITER_COUNT))

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': makeL(i)}) for i in range(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': makeL(i)} for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in range(ITER_COUNT)]

@timeit
def profile_dict_of_slot():
    return dict((i, SlotObj(i)) for i in range(ITER_COUNT))

@timeit
def profile_list_of_slot():
    return [SlotObj(i) for i in range(ITER_COUNT)]

profile_dict_of_nt()
profile_list_of_nt()
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slot()
profile_list_of_slot()

এবং এগুলি আমার ফলাফল

Time Taken = 0:00:07.018720,    provile_dict_of_nt,     Size = 951.83
Time Taken = 0:00:07.716197,    provile_list_of_nt,     Size = 1,084.75
Time Taken = 0:00:03.237139,    profile_dict_of_dict,   Size = 1,926.29
Time Taken = 0:00:02.770469,    profile_list_of_dict,   Size = 1,778.58
Time Taken = 0:00:07.961045,    profile_dict_of_obj,    Size = 1,537.64
Time Taken = 0:00:05.899573,    profile_list_of_obj,    Size = 1,458.05
Time Taken = 0:00:06.567684,    profile_dict_of_slot,   Size = 1,035.65
Time Taken = 0:00:04.925101,    profile_list_of_slot,   Size = 887.49

আমার উপসংহারটি হ'ল:

  1. স্লটে সেরা মেমরির পদচিহ্ন রয়েছে এবং গতিতে যুক্তিসঙ্গত।
  2. ডিক্টস সবচেয়ে দ্রুত, তবে সর্বাধিক স্মৃতি ব্যবহার করে।

ম্যান, আপনার এটিকে একটি প্রশ্নে পরিণত করা উচিত। আমি এটি নিজের কম্পিউটারেও চালিয়েছি, কেবল তা নিশ্চিত করার জন্য (আমার কাছে স্যুইটিল ইনস্টল নেই, তাই আমি সেই অংশটি নিয়েছি)। যাইহোক, এটি আমার কাছে বিস্মিত হচ্ছে এবং এর অর্থ মূল প্রশ্নের সম্পূর্ণ উত্তর দেওয়া হয়নি। অন্যান্য সমস্ত উত্তরগুলি "নেমডটপল দুর্দান্ত" এবং "ব্যবহার স্লট " এর মতো এবং দৃশ্যত একটি ব্র্যান্ড নিউ ডিক বস্তু প্রতিবার তাদের চেয়ে দ্রুত? আমার ধারণা dicts সত্যিই ভাল অনুকূলিত হয়?
মাল্টিহান্টার

1
মনে হচ্ছে এটি একটি স্ট্রিং ফিরিয়ে দেওয়া মেকএল ফাংশনের ফলাফল। আপনি যদি একটি খালি তালিকা ফিরিয়ে দেন, পরিবর্তে, ফলাফলগুলি পাইথন 2 থেকে হাগডব্রাউনগুলির সাথে মোটামুটি মেলে। নামকরণকৃত ব্যতীত সর্বদা স্লটবজ থেকে ধীর গতির :(
মাল্টিহান্টার

একটি ছোট সমস্যা হতে পারে: মেকএল প্রতিটি '@ টাইমাইট' রাউন্ডে বিভিন্ন গতিতে দৌড়াতে পারে যেহেতু অজগরটিতে স্ট্রিং ক্যাশেড রয়েছে - তবে সম্ভবত আমি ভুল করছি।
বার্নি

@ বার্নাবাসজাবলিক্স প্রতিবার একটি নতুন স্ট্রিং তৈরি করা উচিত কারণ এটির মানটি প্রতিস্থাপন করতে হবে "এটি একটি নমুনা স্ট্রিং% s"% i
জারোদ চেসনি

হ্যাঁ, এটি লুপের মধ্যেই সত্য, তবে দ্বিতীয় পরীক্ষায় আমি আবার 0 থেকে শুরু করি।
বার্নি

4
from datetime import datetime

ITER_COUNT = 1000 * 1000

def timeit(method):
    def timed(*args, **kw):
        s = datetime.now()
        result = method(*args, **kw)
        e = datetime.now()

        print method.__name__, '(%r, %r)' % (args, kw), e - s
        return result
    return timed

class Obj(object):
    def __init__(self, i):
       self.i = i
       self.l = []

class SlotObj(object):
    __slots__ = ('i', 'l')
    def __init__(self, i):
       self.i = i
       self.l = []

@timeit
def profile_dict_of_dict():
    return dict((i, {'i': i, 'l': []}) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_dict():
    return [{'i': i, 'l': []} for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_obj():
    return dict((i, Obj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_obj():
    return [Obj(i) for i in xrange(ITER_COUNT)]

@timeit
def profile_dict_of_slotobj():
    return dict((i, SlotObj(i)) for i in xrange(ITER_COUNT))

@timeit
def profile_list_of_slotobj():
    return [SlotObj(i) for i in xrange(ITER_COUNT)]

if __name__ == '__main__':
    profile_dict_of_dict()
    profile_list_of_dict()
    profile_dict_of_obj()
    profile_list_of_obj()
    profile_dict_of_slotobj()
    profile_list_of_slotobj()

ফলাফল:

hbrown@hbrown-lpt:~$ python ~/Dropbox/src/StackOverflow/1336791.py 
profile_dict_of_dict ((), {}) 0:00:08.228094
profile_list_of_dict ((), {}) 0:00:06.040870
profile_dict_of_obj ((), {}) 0:00:11.481681
profile_list_of_obj ((), {}) 0:00:10.893125
profile_dict_of_slotobj ((), {}) 0:00:06.381897
profile_list_of_slotobj ((), {}) 0:00:05.860749

3

প্রশ্ন নেই।
আপনার কাছে ডেটা রয়েছে, অন্য কোনও বৈশিষ্ট্য ছাড়াই (কোনও পদ্ধতি নেই, কিছুই নেই)। অতএব আপনার কাছে একটি ডেটা ধারক রয়েছে (এই ক্ষেত্রে একটি অভিধান)।

আমি সাধারণত ডেটা মডেলিংয়ের ক্ষেত্রে ভাবতে পছন্দ করি । যদি কোনও বিশাল পারফরম্যান্সের সমস্যা থাকে তবে আমি বিমূর্তিতে কিছু দিতে পারি তবে কেবল খুব ভাল কারণেই।
প্রোগ্রামিং জটিলতা পরিচালনা এবং সঠিক বিমূর্ততা রক্ষণাবেক্ষণ সম্পর্কে প্রায়শই এই জাতীয় ফলাফল অর্জনের জন্য সবচেয়ে কার্যকর উপায়।

কোনও জিনিস ধীর হওয়ার কারণগুলি সম্পর্কে , আমি মনে করি আপনার পরিমাপ সঠিক নয়।
লুপটির জন্য আপনি খুব অ্যাসাইনমেন্ট সম্পাদন করছেন, এবং তাই আপনি যা দেখছেন সেখানে ডিক (অন্তর্নিহিত বস্তু) এবং একটি "কাস্টম" অবজেক্ট ইনস্ট্যান্ট করার জন্য আলাদা সময় প্রয়োজন। যদিও ভাষার দৃষ্টিকোণ থেকে তারা একই, তবে তাদের বেশ আলাদা বাস্তবায়ন রয়েছে।
এরপরে, অ্যাসাইনমেন্টের সময় উভয়ের জন্য প্রায় একই হওয়া উচিত, যেমন শেষের সদস্যরা একটি অভিধানের মধ্যে রক্ষণাবেক্ষণ করে।


0

মেমরির ব্যবহার হ্রাস করার আরও একটি উপায় আছে যদি ডেটা স্ট্রাকচারে রেফারেন্স চক্র থাকে না।

দুটি ক্লাসের তুলনা করা যাক:

class DataItem:
    __slots__ = ('name', 'age', 'address')
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

এবং

$ pip install recordclass

>>> from recordclass import structclass
>>> DataItem2 = structclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
DataItem(name='Mike', age=10, address='Cherry Street 15')
64 40

structclassবেসড ক্লাসগুলি চক্রীয় আবর্জনা সংগ্রহকে সমর্থন করে না বলে এটি সম্ভব হয়েছিল , যা এ জাতীয় ক্ষেত্রে প্রয়োজন হয় না।

__slots__বেস-ভিত্তিক শ্রেণীর একটি সুবিধাও রয়েছে : আপনি অতিরিক্ত গুণাবলী যুক্ত করতে সক্ষম হন:

>>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
>>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
>>> inst3.hobby = ['drawing', 'singing']
>>> print(inst3)
>>> print(sizeof(inst3), 'has dict:',  bool(inst3.__dict__))
DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
48 has dict: True

0

@ জারোদ-চেসনির খুব সুন্দর স্ক্রিপ্টের আমার পরীক্ষার রান এখানে। তুলনার জন্য, আমি পাইথন 2 এর বিপরীতে "রেঞ্জ" দ্বারা "এক্সরেঞ্জ" প্রতিস্থাপন করেছি।

কৌতূহল দ্বারা, আমি তুলনা করার জন্য অর্ডারডিক্ট (অর্ডিক্ট) এর সাথেও একই রকম পরীক্ষা যুক্ত করেছি।

পাইথন ৩.6.৯:

Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67

Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75

পাইথন ২.7.১৫+:

Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0

Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0

সুতরাং, উভয় প্রধান সংস্করণে, @ জারোদ-চেসনির সিদ্ধান্তগুলি এখনও ভাল দেখাচ্ছে।

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.