跳转至

尚未翻译

本页面尚未翻译成中文,因此显示英文原文。 帮助翻译

hasattr() Function Complexity

The hasattr() function checks whether an object has a named attribute. It is one getattr() whose AttributeError becomes False, so it costs exactly what the attribute lookup costs: a cached class-level lookup plus an instance dict probe, or whatever a user-defined hook does.

Complexity Reference

Let d be the length of the object's class MRO and g the cost, in time and space, of a user-defined hook: __getattr__, __getattribute__, or a descriptor such as a property getter.

Operation Time Space Notes
hasattr(obj, name) O(1) avg O(1) The class-level lookup is cached per class and name object, for misses as well as hits, so d does not matter once the cache is warm - provided name is the same str object each time, as a literal in the source or a sys.intern() result is
hasattr(obj, name) with a name built at run time O(d) O(1) An f-string, a concatenation or a string read from input is usually a new object on every call, and the cache matches name objects rather than their text, so every call walks the MRO. sys.intern(name) returns one shared object per text and restores the cached cost. A str subclass, or a name longer than 100 characters, is never cached
hasattr(obj, name), first lookup after the class or a base changed O(d) O(1) Walks the MRO once and caches the result. From Python 3.13, once a class has been changed and looked up again more than 1,000 times it is never cached again, and every lookup on it pays O(d)
hasattr(obj, name) through a hook O(g) O(g) On top of the lookup above. A property getter runs, and __getattr__ runs on every miss. Only AttributeError becomes False; any other exception propagates
hasattr(obj, name) with a non-str name O(1) O(1) Raises TypeError before any lookup

Basic Usage

Check Attribute Existence

# O(1) avg - one cached lookup
class MyClass:
    attr = 42

obj = MyClass()

# Safe check - O(1) avg
if hasattr(obj, 'attr'):
    print(obj.attr)  # 42

# A miss is cached too - O(1) avg
if hasattr(obj, 'missing'):
    print("Has missing")
else:
    print("No missing attribute")

Avoid AttributeError

# All three forms do the same lookup; they differ in how many times they do it
class User:
    name = "Alice"

user = User()

# try/except - one lookup
try:
    email = user.email
except AttributeError:
    email = "no@email.com"

# hasattr() then access - two lookups when the attribute exists
if hasattr(user, 'email'):
    email = user.email
else:
    email = "no@email.com"

# getattr() with a default - one lookup either way, and the clearest
email = getattr(user, 'email', "no@email.com")

Complexity Details

Inheritance Chain Traversal

# The first lookup of a name on a class walks the MRO - O(d)
# Later lookups of that name on that class are cached - O(1) avg
class A:
    a_attr = 1

class B(A):
    b_attr = 2

class C(B):
    c_attr = 3

class D(C):
    d_attr = 4

obj = D()

# MRO: [D, C, B, A, object]
has_a = hasattr(obj, 'a_attr')  # O(d) once: D, C, B, then found in A
has_a = hasattr(obj, 'a_attr')  # O(1) avg: cached for (D, 'a_attr')
has_x = hasattr(obj, 'x_attr')  # O(d) once, then the miss is cached too

# Assigning to a class, or to any of its bases, invalidates its cache
C.c_attr = 30
has_a = hasattr(obj, 'a_attr')  # O(d) again, once

Names Built at Run Time

# The cache matches the name object, not its text
import sys

class Shape:
    def draw_circle(self):
        pass

def supports(obj, kind):
    # The f-string is a new object per call - every call walks the MRO, O(d)
    return hasattr(obj, f"draw_{kind}")

def supports_interned(obj, kind):
    # One shared object per text - the lookup is O(1) avg once the cache is warm
    return hasattr(obj, sys.intern(f"draw_{kind}"))

shape = Shape()
assert supports(shape, "circle") and not supports(shape, "square")
assert supports_interned(shape, "circle") and not supports_interned(shape, "square")

What hasattr() Catches

# hasattr() is one getattr() call; only AttributeError means False
def hasattr_simulation(obj, name):
    """What hasattr() does - the cost is the lookup's"""
    if not isinstance(name, str):
        raise TypeError("attribute name must be string")
    try:
        getattr(obj, name)
    except AttributeError:
        return False
    return True

class Strict:
    @property
    def broken(self):
        raise KeyError("not an AttributeError")

obj = Strict()
print(hasattr(obj, 'missing'))  # False

try:
    hasattr(obj, 'broken')  # the KeyError propagates
except KeyError:
    print("only AttributeError means False")

try:
    hasattr(obj, 42)
except TypeError:
    print("the name must be a str")

Performance Patterns

hasattr vs getattr with Default

# hasattr() then getattr() - two lookups
class Config:
    host = "localhost"

obj = Config()

if hasattr(obj, 'host'):  # lookup one
    value = getattr(obj, 'host')  # lookup two
else:
    value = None

# getattr() with a default - one lookup
value = getattr(obj, 'host', None)

Checking Multiple Attributes

# O(n) - one cached lookup per name checked
class Message:
    to = "user@example.com"
    subject = "Hello"
    body = "Content"

msg = Message()

required = ['to', 'subject', 'body']
valid = all(hasattr(msg, attr) for attr in required)  # O(n) - n names

# dir() is not a shortcut: it collects and sorts every attribute of the
# object and its whole MRO, so it grows with that attribute count where a
# few hasattr() calls do not
valid = {'to', 'subject', 'body'} <= set(dir(msg))  # at least O(a) in the a names listed

Common Use Cases

Optional Feature Detection

# O(1) avg - check if feature is available
class DataStore:
    def load(self):
        pass

    def save(self):
        pass
    # backup() is optional

class DatabaseStore(DataStore):
    def backup(self):
        """Advanced feature"""
        pass

store = DatabaseStore()

# O(1) avg - check for optional method
if hasattr(store, 'backup'):
    store.backup()  # Call if available

Protocol Checking

# One cached lookup per protocol method
def is_iterable(obj):
    """Check for the iterator protocol - one lookup"""
    return hasattr(obj, '__iter__')

def is_context_manager(obj):
    """Check for the context manager protocol - one or two lookups"""
    return hasattr(obj, '__enter__') and hasattr(obj, '__exit__')

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

obj = Countdown(3)

if is_iterable(obj):
    for item in obj:
        print(item)  # 3, 2, 1

Safe Method Invocation

# hasattr() then the call - two lookups when the method exists
class Handler:
    def process(self, data):
        return f"Processed: {data}"

class MinimalHandler:
    pass

def safe_process(obj, data):
    """Call method if it exists"""
    if hasattr(obj, 'process'):  # O(1) avg
        return obj.process(data)
    return None

handler = Handler()
minimal = MinimalHandler()

result1 = safe_process(handler, "data")    # Processed: data
result2 = safe_process(minimal, "data")    # None

Advanced Usage

Dynamic Behavior Routing

# Route to a handler by name
class Plugin:
    def on_start(self):
        pass

    def on_stop(self):
        pass

class AdvancedPlugin(Plugin):
    def on_update(self):
        pass

def trigger_event(obj, event_name):
    """Safely trigger an event - one lookup"""
    handler = getattr(obj, event_name, None)
    if handler is not None:
        handler()

plugin = AdvancedPlugin()

trigger_event(plugin, 'on_start')   # Works
trigger_event(plugin, 'on_update')  # Works
trigger_event(plugin, 'missing')    # No error

Attribute Validation

# O(n) - one lookup per required name
def validate_object(obj, required_attrs):
    """Return the required names the object lacks - O(n)"""
    missing = []

    for attr in required_attrs:  # n iterations
        if not hasattr(obj, attr):  # O(1) avg each
            missing.append(attr)

    return missing if missing else None

class FileWriter:
    def write(self):
        pass

    def close(self):
        pass

required = ['write', 'close', 'flush']
missing = validate_object(FileWriter(), required)
# Returns: ['flush'] - missing method

Duck Typing Implementation

# Two lookups per capability used: the check and the call
def transform(data):
    return data.upper()

def process_data(obj):
    """Work with any object that has the required methods"""
    if hasattr(obj, 'read'):
        data = obj.read()
    else:
        raise TypeError("Object doesn't support read")

    if hasattr(obj, 'write'):
        obj.write(transform(data))
    else:
        raise TypeError("Object doesn't support write")

# Works with file-like objects
import io
buffer = io.StringIO("content")

process_data(buffer)
print(buffer.getvalue())  # contentCONTENT

Practical Examples

Conditional Initialization

# Up to three lookups: two checks and one read
class FlexibleConfig:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def get_port(self):
        """Get port with fallback"""
        if hasattr(self, 'port'):  # O(1) avg
            return self.port
        elif hasattr(self, 'default_port'):  # O(1) avg
            return self.default_port
        else:
            return 8000

config = FlexibleConfig(default_port=9000)
port = config.get_port()  # 9000

Plugin System

# O(n) - one lookup per feature name
class PluginManager:
    def get_capabilities(self, plugin):
        """List plugin capabilities - O(n)"""
        capabilities = []

        features = ['process', 'validate', 'transform', 'cache']

        for feature in features:  # n iterations
            if hasattr(plugin, feature):  # O(1) avg each
                capabilities.append(feature)

        return capabilities

class SimplePlugin:
    def process(self):
        pass

    def validate(self):
        pass

manager = PluginManager()
caps = manager.get_capabilities(SimplePlugin())
# Returns: ['process', 'validate']

Edge Cases

Descriptors Run Inside hasattr()

# hasattr() finds attributes, methods and properties alike - and runs getters
class Example:
    value = 42  # Attribute

    def method(self):  # Callable
        pass

    @property
    def computed(self):  # Property - its getter runs on every hasattr()
        print("computing")
        return 100

obj = Example()

print(hasattr(obj, 'value'))      # True - attribute
print(hasattr(obj, 'method'))     # True - callable
print(hasattr(obj, 'computed'))   # prints "computing", then True

Side Effects of getattr

# __getattr__ runs on every miss - its cost and side effects are hasattr()'s
class WithSideEffect:
    def __getattr__(self, name):
        print(f"Looking up: {name}")
        raise AttributeError(name)

obj = WithSideEffect()

result = hasattr(obj, 'missing')  # Prints "Looking up: missing"; result is False

# A __getattr__ that returns instead of raising makes every name exist
class Permissive:
    def __getattr__(self, name):
        return None

print(hasattr(Permissive(), 'anything'))  # True

Best Practices

✅ Do:

  • Use hasattr() to test for an attribute before acting on it
  • Use getattr() with a default when the value is needed - it is one lookup, not two
  • Use it for duck typing and protocol checking
  • Expect property getters and __getattr__ to run inside it
  • Pass a name built at run time through sys.intern() when it is checked repeatedly on a deep hierarchy - a fresh string object misses the lookup cache

❌ Avoid:

  • hasattr() followed by getattr() on the same name
  • dir() as a way to check for a few names - it builds and sorts the whole attribute list
  • Relying on hasattr() to swallow anything but AttributeError
  • Reassigning a class's attributes in a hot loop on Python 3.13 or later - after more than 1,000 rounds of change and lookup the class is never cached again

Version Notes

  • Python 2.x: Every Exception raised by the lookup, not only AttributeError, is swallowed and reported as False
  • Python 3.2+: Only AttributeError becomes False; every other exception propagates
  • Python 3.13+: A class that has been changed and looked up again more than 1,000 times stops being cached, so lookups on it are O(d) from then on
  • Python 3: A non-str name, bytes included, raises TypeError